Class: CollectingEvent
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- CollectingEvent
- Includes:
- DwcSerialization, GeoLocate, Georeference, Housekeeping, Shared::Citations, Shared::Confidences, Shared::DataAttributes, Shared::Depictions, Shared::Documentation, Shared::DwcOccurrenceHooks, Shared::HasPapertrail, Shared::Identifiers, Shared::IsData, Shared::Labels, Shared::Notes, Shared::ProtocolRelationships, Shared::QueryBatchUpdate, 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.
Defined Under Namespace
Modules: DwcSerialization, GeoLocate, Georeference
Constant Summary collapse
- NEARBY_DISTANCE =
5000
- MINIMUM_ELEVATION =
-11000
- MAXIMUM_ELEVATION =
8500
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.
-
#geographic_names ⇒ Object
getter for attr :geographic_names.
-
#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.
-
#meta_prioritize_geographic_area ⇒ Boolean?
A meta attribute.
-
#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.
-
#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_field_number ⇒ 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.
-
#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.
-
#with_verbatim_data_georeference ⇒ Object
Returns the value of attribute with_verbatim_data_georeference.
Class Method Summary collapse
- .batch_update(params) ⇒ Hash, false
-
.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/filter.
-
.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.
- #check_date_range ⇒ Object protected
- #check_elevation_range ⇒ Object protected
- #check_ma_range ⇒ Object protected
- #check_max_land_elevation ⇒ Object protected
- #check_min_land_elevation ⇒ Object protected
- #check_verbatim_geolocation_uncertainty ⇒ Object protected
-
#clone(annotations: false, incremented_identifier_id: nil) ⇒ 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.
- #dwc_occurrences ⇒ Object
-
#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 ??.
-
#end_day_of_year ⇒ Integer?
The end day of the event from 1-365 (or 366 in leap years) only returned when completely unambigouous, in theory could be added for month/day combinations on, but that is uncertain.
-
#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?}.
-
#geo_json_data ⇒ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil]
A shape to represent the CE, prioritize georeference over geographic_area.
-
#geographic_area_default_geographic_item ⇒ GeographicItem?
Returns the geographic_item corresponding to the geographic area, if provided.
-
#geographic_area_default_geographic_item_id ⇒ id?
Returns the geographic_item corresponding to the geographic area, if provided.
-
#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_uncertainty_in_meters ⇒ Scalar (Int, Float, etc)?
-
#get_geographic_name_classification ⇒ Object
This prioritizes Georeferences over GeographicAreas!.
- #has_cached_geographic_names? ⇒ Boolean
- #has_collectors? ⇒ Boolean
-
#has_data? ⇒ Boolean
TODO: consider renaming, reference new Merge code.
-
#has_end_date? ⇒ Boolean
Has a fully defined date.
- #has_some_date? ⇒ Boolean
-
#has_start_date? ⇒ Boolean
Has a fully defined date.
- #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.
- #set_cached ⇒ Object protected
-
#set_cached_cached ⇒ Object
protected
TODO: A time sync.
- #set_cached_geographic_names ⇒ 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 and move to filter.
- #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 ??.
-
#start_day_of_year ⇒ Integer?
The start day of the event from 1-365 (or 366 in leap years) only returned when completely unambigouous, in theory could be added for month/day combinations on, but that is uncertain.
- #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
- #sv_missing_geographic_area ⇒ Object protected
- #sv_missing_georeference ⇒ Object protected
- #sv_verbatim_uncertainty_format ⇒ 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
TODO: DRY with helper methods, these are now outdated approaches.
-
#to_simple_json_feature ⇒ Object
TODO: DRY with helper methods, these are now outdated approaches i.e.
-
#verbatim_map_center(delta_z = 0.0) ⇒ RGeo::Geographic::ProjectedPointImpl?
For the verbatim latitude/longitude only.
Methods included from Shared::QueryBatchUpdate
Methods included from DwcSerialization
Methods included from Georeference
#dwc_georeference_source, #georeference_latitude, #georeference_longitude, #get_error_radius, #latitude, #longitude, #next_without_georeference, #verbatim_center_coordinates
Methods included from GeoLocate
#geolocate_attributes, #geolocate_ui_params, #geolocate_ui_params_string, #lat_long_source
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 SoftValidation
#clear_soft_validations, #fix_for, #fix_soft_validations, #soft_fixed?, #soft_valid?, #soft_validate, #soft_validated?, #soft_validations, #soft_validators
Methods included from Shared::HasPapertrail
#attribute_updated, #attribute_updater
Methods included from Shared::ProtocolRelationships
#protocolled?, #reject_protocols
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
#dwc_occurrence_id, #identified?, #next_by_identifier, #previous_by_identifier, #reject_identifiers, #uri, #uuid
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.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#cached_level0_geographic_name ⇒ String?
Returns the auto-calculated level0 (= country in TaxonWorks) value drawn from GeographicNames, never directly user supplied.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#cached_level1_geographic_name ⇒ String?
Returns the auto-calculated level1 (typically state/province) value drawn from GeographicNames, never directly user supplied.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#cached_level2_geographic_name ⇒ String?
Returns the auto-calculated level2 value (e.g. county) drawn from GeographicNames, never directly user supplied.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end 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.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#elevation_precision ⇒ String
A float, in meters.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#end_date_day ⇒ Integer
Returns the date of the month the collecting event ended on.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#end_date_month ⇒ Integer
Returns the month, from 0-12, that the collecting event ended on.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#end_date_year ⇒ Integer
Returns the four digit year, end of the collecting event.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#field_notes ⇒ String
Any/all field notes that this collecting event was derived from, or that supplement this collecting event.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#formation ⇒ String?
Returns formation sensu PBDB.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#geographic_area_id ⇒ Integer
Returns the finest geo-political unit that this collecting event can be localized to, can be used for gross georeferencing when Georeference not available.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#geographic_names ⇒ Object
getter for attr :geographic_names
222 223 224 |
# File 'app/models/collecting_event.rb', line 222 def geographic_names @geographic_names end |
#group ⇒ String?
Returns member sensu PBDB.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#lithology ⇒ String?
Returns lithology sensu PBDB.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#max_ma ⇒ Decimal?
Returns max_ma (million years) sensu PBDB.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#maximum_elevation ⇒ String
A float, in meters.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#md5_of_verbatim_label ⇒ String
Returns application defined, an index to the verbatim label.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#member ⇒ String?
Returns member sensu PBDB.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#meta_prioritize_geographic_area ⇒ Boolean?
Returns A meta attribute. When true then DwcOccurence indexing will use the geographic name hierarchy from GeographicArea, not as inferred by geospatial (Georeference) calculation. The is used when reverse georefencing fails because of the lack of precision of the GeographicItem (shapes) for GeographicAreas.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#min_ma ⇒ Decimal?
Returns min_ma (million years) sensu PBDB.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#minimum_elevation ⇒ String
A float, in meters.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#no_cached ⇒ Boolean
Returns When true, cached values are not built.
218 219 220 |
# File 'app/models/collecting_event.rb', line 218 def no_cached @no_cached end |
#print_label ⇒ String
A print-formatted ready representation of this collecting event. !! Do not assume that this remains static, it can change over time with user needs.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#project_id ⇒ Integer
the project ID
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#start_date_day ⇒ Integer
Returns the day of the month the collecting event started on.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#start_date_month ⇒ Integer
Returns the month, from 0-12, that the collecting event started on.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#start_date_year ⇒ Integer
Returns the four digit year, start of the collecting event.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#time_end_hour ⇒ Integer
Returns 0-23.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#time_end_minute ⇒ Integer
Returns 0-59.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#time_end_second ⇒ Integer
Returns 0-59.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#time_start_hour ⇒ Integer
Returns 0-23.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#time_start_minute ⇒ Integer
Returns 0-59.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#time_start_second ⇒ Integer
Returns 0-59.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#verbatim_collectors ⇒ String
Returns the literal string that indicates the collectors, typically taken right off the label.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#verbatim_date ⇒ String
Returns the string representation, typically as taken from the label, of the date.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#verbatim_datum ⇒ String
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#verbatim_elevation ⇒ String
A string, typically sliced from verbatim_label, that represents all elevation data (min/max/precision) as recorded there.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#verbatim_field_number ⇒ String
Returns the literal string/identifier used by the collector(s) to identify this particular collecting event, usually part of a series particular to one trip.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#verbatim_geolocation_uncertainty ⇒ String
A string, typically sliced from verbatim_label, that represents the provided uncertainty value.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#verbatim_habitat ⇒ String
Returns a literal string, typically taken from the printed label, tha represents assertions about the habitat.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#verbatim_label ⇒ String
A verbatim representation of label that defined this collecting event, typically, but not exclusively, used for retroactive data capture.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#verbatim_latitude ⇒ String
A string, typically sliced from verbatim_label, that represents the latitude. Is used to derive mappable values, but does not get mapped itself.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#verbatim_locality ⇒ String
Returns a string, typically sliced from verbatim_label, that represents the locality, including any modifiers (2 mi NE).
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#verbatim_longitude ⇒ String
A string, typically sliced from verbatim_label, that represents the longitude. Is used to derive mappable values, but does not get mapped itself
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#verbatim_method ⇒ String
Returns the literal string that indicates the collecting method, typically taken right off the label.
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 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 |
# File 'app/models/collecting_event.rb', line 183 class CollectingEvent < ApplicationRecord include Housekeeping include Shared::Citations include Shared::DataAttributes include Shared::Identifiers include Shared::Notes include Shared::Tags include Shared::Depictions include Shared::Labels include Shared::Confidences include Shared::Documentation include Shared::ProtocolRelationships include Shared::HasPapertrail include SoftValidation include Shared::Labels include Shared::DwcOccurrenceHooks include Shared::IsData include CollectingEvent::GeoLocate include CollectingEvent::Georeference include CollectingEvent::DwcSerialization include Shared::QueryBatchUpdate ignore_whitespace_on(:document_label, :verbatim_label, :print_label) NEARBY_DISTANCE = 5000 MINIMUM_ELEVATION = -11000 MAXIMUM_ELEVATION = 8500 attr_accessor :with_verbatim_data_georeference # @return [Boolean] # When true, cached values are not built attr_accessor :no_cached # @return [Hash] # of known country/state/county values attr_accessor :geographic_names # See also CollectingEvent::GeoLocate belongs_to :geographic_area, inverse_of: :collecting_events has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy, inverse_of: :role_object has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collection_objects, inverse_of: :collecting_event, dependent: :restrict_with_error has_many :collector_roles, class_name: 'Collector', as: :role_object, dependent: :destroy, inverse_of: :role_object has_many :collectors, -> { order('roles.position ASC') }, through: :collector_roles, source: :person, inverse_of: :collecting_events # see also app/models/collecting_event/georeference.rb for more has_many has_many :otus, -> { unscope(:order) }, through: :collection_objects, source: 'otu' has_many :field_occurrences, inverse_of: :collecting_event 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 :set_cached, unless: -> { no_cached } # See also app/models/collecting_event/georeference.rb for more accepts_nested_attributes accepts_nested_attributes_for :collectors, :collector_roles, allow_destroy: true validate :check_verbatim_geolocation_uncertainty, :check_date_range, :check_elevation_range, :check_min_land_elevation, :check_max_land_elevation, :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.present? } validates_presence_of :verbatim_latitude, if: -> { verbatim_longitude.present? } 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.present? } validates_presence_of :time_start_hour, if: -> { self.time_start_minute.present? } 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.present? } validates_presence_of :time_end_hour, if: -> { self.time_end_minute.present? } 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? } validates_presence_of :geographic_area_id, if: -> { } soft_validate( :sv_minimally_check_for_a_label, set: :minimally_check_for_a_label, name: 'Minimally check for a label', description: 'At least one label type, or field notes, should be provided') soft_validate( :sv_missing_georeference, set: :georeference, name: 'Missing georeference', description: 'Georeference is missing') soft_validate( :sv_georeference_matches_verbatim, set: :georeference, name: 'Georeference matches verbatim', description: 'Georeference matches verbatim latitude and longitude') soft_validate( :sv_missing_geographic_area, set: :georeference, name: 'Missing geographic area', description: 'Georaphic area is missing') def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end # @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: { updated_at: 1.week.ago..Time.now } ).order('"collection_objects"."updated_at" DESC') } scope :used_in_project, -> (project_id) { joins(:collection_objects).where( collection_objects: { 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:, 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_facet.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/filter def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 and move to filter def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end # @return [Boolean] # test for minimal data # TODO: consider renaming, reference new Merge code def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end def has_collectors? verbatim_collectors.present? || collectors.any? end # @return [Boolean] # has a fully defined date def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end # @return [Boolean] # has a fully defined date def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? 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 [Integer, nil] # the start day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end # @return [Integer, nil] # the end day of the event from 1-365 (or 366 in leap years) # only returned when completely unambigouous, in theory could # be added for month/day combinations on, but that is uncertain def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid 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:) else nil end 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 # @return [id, nil] # returns the geographic_item corresponding to the geographic area, if provided def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end # @return [ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil] ] # a shape to represent the CE, # prioritize georeference over geographic_area def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end 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 def has_cached_geographic_names? geographic_names != {} end # getter for attr :geographic_names def geographic_names return @geographic_names if !@geographic_names.nil? @geographic_names ||= { country: cached_level0_geographic_name, state: cached_level1_geographic_name, county: cached_level2_geographic_name }.delete_if{|k,v| v.nil?} # TODO: Once cached index is updated this should be removed, it is # costly for records with geographic_area_id without country, state, county if @geographic_names == {} && geographic_area_id r = get_geographic_name_classification set_cached_geographic_names end @geographic_names ||= {} end # @return Hash # a geographic_name_classification. # This prioritizes Georeferences over GeographicAreas! def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 # @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 # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 # TODO: DRY with helper methods, these are now outdated approaches # @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: DRY with helper methods, these are now outdated approaches # 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 # @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&.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.centroid else nil end end def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } 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_level2_name if cached_level2_name # cache_geographic_names[:county] #end # def cached_level0_name # return cached_level0_name if cached_level0_name # cache_geographic_names[:country] # end # @return [CollectingEvent] # the instance may not be valid! def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end 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 # @return [Scalar (Int, Float, etc), nil] def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end class << self # @return [Hash, false] def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end end protected def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end # TODO: A time sync. def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) 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.present? && 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_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? 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_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? 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).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end end |
#with_verbatim_data_georeference ⇒ Object
Returns the value of attribute with_verbatim_data_georeference.
214 215 216 |
# File 'app/models/collecting_event.rb', line 214 def with_verbatim_data_georeference @with_verbatim_data_georeference end |
Class Method Details
.batch_update(params) ⇒ Hash, false
1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 |
# File 'app/models/collecting_event.rb', line 1069 def batch_update(params) request = QueryBatchRequest.new( klass: 'CollectingEvent', object_filter_params: params[:collecting_event_query], object_params: params[:collecting_event], async_cutoff: params[:async_cutoff] || 26, preview: params[:preview], ) request.cap = 1000 request.cap_reason = 'Max 500 updated at a time.' query_batch_update(request) end |
.contained_within(geographic_item) ⇒ Scope
TODO: use joins(:geographic_items).where(containing scope), simplied to
368 369 370 |
# File 'app/models/collecting_event.rb', line 368 def contained_within(geographic_item) CollectingEvent.joins(:geographic_items).where(GeographicItem.contained_by_where_sql(geographic_item.id)) end |
.filter_by(params) ⇒ Scope
TODO: deprecate for lib/queries/collecting_event/filter
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 |
# File 'app/models/collecting_event.rb', line 397 def filter_by(params) sql_string = '' if params.present? # 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 = '' if v_locality_fragment.present? if sql_string.present? prefix = ' and ' end sql_string += "#{ prefix }verbatim_locality ilike '%#{v_locality_fragment}%'" end prefix = '' if any_label_fragment.present? if sql_string.present? 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 if id_fragment.present? # @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 |
.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
388 389 390 391 392 |
# File 'app/models/collecting_event.rb', line 388 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_facet.to_sql).distinct # TODO: uniq should likely not be here end |
.not_including(collecting_events) ⇒ Scope
TODO: DRY, use general form of this
375 376 377 |
# File 'app/models/collecting_event.rb', line 375 def not_including(collecting_events) where.not(id: collecting_events) end |
.select_optimized(user_id, project_id) ⇒ Object
Scopes
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 |
# File 'app/models/collecting_event.rb', line 347 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:, 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 |
Instance Method Details
#all_geographic_items ⇒ Scope
Returns all geographic_items associated with this collecting_event through georeferences only.
608 609 610 611 612 613 |
# File 'app/models/collecting_event.rb', line 608 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 |
#build_verbatim_geographic_item ⇒ GeographicItem?
Returns a GeographicItem instance representing a translation of the verbatim values, not saved.
594 595 596 597 598 599 600 601 602 603 604 |
# File 'app/models/collecting_event.rb', line 594 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:) else nil end end |
#check_date_range ⇒ Object (protected)
1147 1148 1149 1150 1151 1152 1153 1154 |
# File 'app/models/collecting_event.rb', line 1147 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 |
#check_elevation_range ⇒ Object (protected)
1175 1176 1177 |
# File 'app/models/collecting_event.rb', line 1175 def check_elevation_range errors.add(:maximum_elevation, 'Maximum elevation is lower than minimum elevation.') if minimum_elevation.present? && maximum_elevation.present? && maximum_elevation < minimum_elevation end |
#check_ma_range ⇒ Object (protected)
1156 1157 1158 |
# File 'app/models/collecting_event.rb', line 1156 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 |
#check_max_land_elevation ⇒ Object (protected)
1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 |
# File 'app/models/collecting_event.rb', line 1160 def check_max_land_elevation m = 'This is Earth, not Bespin, contact us sky collector.' if (maximum_elevation.present? && maximum_elevation > MAXIMUM_ELEVATION) errors.add(:maximum_elevation, m) end if (minimum_elevation.present? && minimum_elevation > MAXIMUM_ELEVATION) # 2023 LLM errors.add(:minimum_elevation, m) end end |
#check_min_land_elevation ⇒ Object (protected)
1171 1172 1173 |
# File 'app/models/collecting_event.rb', line 1171 def check_min_land_elevation errors.add(:minimum_elevation, 'You know a deeper trench than we do, contact us.') if minimum_elevation.present? && minimum_elevation < MINIMUM_ELEVATION # 2023 LLM end |
#check_verbatim_geolocation_uncertainty ⇒ Object (protected)
1143 1144 1145 |
# File 'app/models/collecting_event.rb', line 1143 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.present? && verbatim_longitude.blank? && verbatim_latitude.blank? end |
#clone(annotations: false, incremented_identifier_id: nil) ⇒ CollectingEvent
Returns the instance may not be valid!.
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 |
# File 'app/models/collecting_event.rb', line 1000 def clone(annotations: false, incremented_identifier_id: nil) a = dup CollectingEvent.transaction do begin a.created_by_id = nil if a.verbatim_label.present? a.verbatim_label = [verbatim_label, "[CLONED FROM #{id}", "at #{Time.now}]"].compact.join(' ') end 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| i = g.dup g.georeferencer_roles.each do |r| i.georeferencer_roles.build(person: r.person, position: r.position) end a.georeferences << i end end if incremented_identifier_id add_incremented_identifier(to_object: a, incremented_identifier_id:) end if !annotations.blank? # TODO: boolean param this clone_annotations(to_object: a, except: [:identifiers]) end a.save! # TODO: confirm behaviour is OK in case of comprehensive. rescue ActiveRecord::RecordInvalid raise ActiveRecord::Rollback end a end end |
#collecting_events_contained_in_error ⇒ Scope
Find other CEs that have GRs whose GIs or EGIs are contained in the EGI
681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 |
# File 'app/models/collecting_event.rb', line 681 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 |
#collecting_events_intersecting_with ⇒ Scope
Find all (other) CEs which have GIs or EGIs (through georeferences) which intersect self
664 665 666 667 668 669 670 671 672 673 674 675 676 |
# File 'app/models/collecting_event.rb', line 664 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 |
#collecting_events_within_radius_of(distance) ⇒ Scope
652 653 654 655 656 657 658 659 660 |
# File 'app/models/collecting_event.rb', line 652 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 |
#collector_names ⇒ String?
yes, it’s a Helper
1050 1051 1052 |
# File 'app/models/collecting_event.rb', line 1050 def collector_names [Utilities::Strings.(collectors.collect{|a| a.last_name}), verbatim_collectors].compact.first end |
#containing_geographic_items ⇒ Array of GeographicItems containing this target
Returns GeographicItems are those that contain either the georeference or, if there are none, the geographic area.
819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 |
# File 'app/models/collecting_event.rb', line 819 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 |
#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
860 861 862 |
# File 'app/models/collecting_event.rb', line 860 def counties_hash name_hash(GeographicAreaType::COUNTY_LEVEL_TYPES) end |
#countries_hash ⇒ Hash
842 843 844 |
# File 'app/models/collecting_event.rb', line 842 def countries_hash name_hash(GeographicAreaType::COUNTRY_LEVEL_TYPES) end |
#country_name ⇒ String
881 882 883 |
# File 'app/models/collecting_event.rb', line 881 def country_name name_from_geopolitical_hash(countries_hash) end |
#county_or_equivalent_name ⇒ String Also known as: county_name
896 897 898 |
# File 'app/models/collecting_event.rb', line 896 def county_or_equivalent_name name_from_geopolitical_hash(counties_hash) end |
#date_range ⇒ Array
Returns date_start and end if provided.
564 565 566 |
# File 'app/models/collecting_event.rb', line 564 def date_range [start_date_string, end_date_string].compact end |
#distance_to(geographic_item_id) ⇒ String
Returns see how far away we are from another gi.
646 647 648 |
# File 'app/models/collecting_event.rb', line 646 def distance_to(geographic_item_id) GeographicItem.distance_between(preferred_georeference.geographic_item_id, geographic_item_id) end |
#dwc_occurrences ⇒ Object
322 323 324 325 326 327 328 329 330 |
# File 'app/models/collecting_event.rb', line 322 def dwc_occurrences # Through CollectionObjects DwcOccurrence .joins("JOIN collection_objects co on dwc_occurrence_object_id = co.id AND dwc_occurrence_object_type = 'CollectionObject'") .where(co: {collecting_event_id: id}) .distinct # TODO: Throuch FieldOccurrence end |
#end_date ⇒ Time
Returns This is for the purposes of computation, not display!.
534 535 536 |
# File 'app/models/collecting_event.rb', line 534 def end_date Utilities::Dates.nomenclature_date(end_date_day, end_date_month, end_date_year) end |
#end_date_string ⇒ String?
Returns an umabigously formatted string with missing parts indicated by ??.
528 529 530 |
# File 'app/models/collecting_event.rb', line 528 def end_date_string Utilities::Dates.from_parts(end_date_year, end_date_month, end_date_day) if some_end_date? end |
#end_day_of_year ⇒ Integer?
Returns the end day of the event from 1-365 (or 366 in leap years) only returned when completely unambigouous, in theory could be added for month/day combinations on, but that is uncertain.
515 516 517 518 |
# File 'app/models/collecting_event.rb', line 515 def end_day_of_year return unless has_end_date? Date.new(end_date_year, end_date_month, end_date_day).yday end |
#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?}
574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 |
# File 'app/models/collecting_event.rb', line 574 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:} vg_attributes.merge!(by: creator.id, project_id:) if reference_self a = ::Georeference::VerbatimData.new(vg_attributes) if a.valid? a.save end return a end rescue ActiveRecord::RecordInvalid raise end false end |
#geo_json_data ⇒ [GeoJSON, Georeference|GeographicArea, object_id ], [nil, nil, nil]
Returns a shape to represent the CE, prioritize georeference over geographic_area.
633 634 635 636 637 638 639 640 641 |
# File 'app/models/collecting_event.rb', line 633 def geo_json_data if a = preferred_georeference # preferred_georeference_geographic_item_id # NOT RIGHT return Gis::GeoJSON.quick_geo_json( a.geographic_item_id ), 'Georeference', a.id elsif b = geographic_area_default_geographic_item_id return Gis::GeoJSON.quick_geo_json(b), 'GeographicArea', geographic_area_id else return nil, nil, nil end end |
#geographic_area_default_geographic_item ⇒ GeographicItem?
Returns the geographic_item corresponding to the geographic area, if provided
617 618 619 |
# File 'app/models/collecting_event.rb', line 617 def geographic_area_default_geographic_item try(:geographic_area).try(:default_geographic_item) end |
#geographic_area_default_geographic_item_id ⇒ id?
Returns the geographic_item corresponding to the geographic area, if provided
623 624 625 626 627 628 |
# File 'app/models/collecting_event.rb', line 623 def geographic_area_default_geographic_item_id GeographicAreasGeographicItem.where(geographic_area_id:) .default_geographic_item_data .pluck(:geographic_item_id) .first end |
#geographic_name_classification_method ⇒ Symbol?
Returns determines (prioritizes) the method to be used to decided the geographic name classification (string labels for country, state, county) for this collecting_event.
805 806 807 808 809 810 811 812 813 814 |
# File 'app/models/collecting_event.rb', line 805 def geographic_name_classification_method # Over-ride first return :geographic_area if return :preferred_georeference if !preferred_georeference.nil? 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 |
#geolocate_uncertainty_in_meters ⇒ Scalar (Int, Float, etc)?
1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 |
# File 'app/models/collecting_event.rb', line 1055 def geolocate_uncertainty_in_meters if verbatim_geolocation_uncertainty.present? begin a = verbatim_geolocation_uncertainty.to_unit return a.convert_to('m').scalar if (a =~ '1 m'.to_unit) rescue ArgumentError end end nil end |
#get_geographic_name_classification ⇒ Object
This prioritizes Georeferences over GeographicAreas!
780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 |
# File 'app/models/collecting_event.rb', line 780 def get_geographic_name_classification case geographic_name_classification_method when :preferred_georeference r = preferred_georeference.geographic_item.geographic_name_hierarchy when :geographic_area_with_shape # geographic_area.try(:has_shape?) # very quick r = geographic_area.geographic_name_classification # do not round trip to the geographic_item, it just points back to the geographic area # slow # rarely hit, this uses the geographic_area shape itself to then find what contains it, and then classify using that # it will by definition be partial 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 |
#has_cached_geographic_names? ⇒ Boolean
753 754 755 |
# File 'app/models/collecting_event.rb', line 753 def has_cached_geographic_names? geographic_names != {} end |
#has_collectors? ⇒ Boolean
478 479 480 |
# File 'app/models/collecting_event.rb', line 478 def has_collectors? verbatim_collectors.present? || collectors.any? end |
#has_data? ⇒ Boolean
TODO: consider renaming, reference new Merge code
466 467 468 469 470 471 472 |
# File 'app/models/collecting_event.rb', line 466 def has_data? CollectingEvent.core_attributes.each do |a| return true if self.send(a).present? end return true if georeferences.any? false end |
#has_end_date? ⇒ Boolean
Returns has a fully defined date.
490 491 492 |
# File 'app/models/collecting_event.rb', line 490 def has_end_date? end_date_day.present? && end_date_month.present? && end_date_year.present? end |
#has_some_date? ⇒ Boolean
474 475 476 |
# File 'app/models/collecting_event.rb', line 474 def has_some_date? verbatim_date.present? || some_start_date? || some_end_date? end |
#has_start_date? ⇒ Boolean
Returns has a fully defined date.
484 485 486 |
# File 'app/models/collecting_event.rb', line 484 def has_start_date? start_date_day.present? && start_date_month.present? && start_date_year.present? end |
#map_center ⇒ Rgeo::Geographic::ProjectedPointImpl?
961 962 963 964 965 966 967 968 969 970 971 972 |
# File 'app/models/collecting_event.rb', line 961 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.centroid else nil end end |
#map_center_method ⇒ Symbol?
Returns the name of the method that will return an Rgeo object that represent the “preferred” centroid for this collecting event.
953 954 955 956 957 958 |
# File 'app/models/collecting_event.rb', line 953 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&.has_shape? nil end |
#name_from_geopolitical_hash(name_hash) ⇒ String
866 867 868 869 870 871 872 873 874 875 876 877 878 |
# File 'app/models/collecting_event.rb', line 866 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 |
#name_hash(types) ⇒ Hash
this needs more work, possibily direct AREL table manipulation.
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
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 |
# File 'app/models/collecting_event.rb', line 727 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 |
#names ⇒ Object
974 975 976 |
# File 'app/models/collecting_event.rb', line 974 def names geographic_area.nil? ? [] : geographic_area.self_and_ancestors.where("name != 'Earth'").collect { |ga| ga.name } end |
#nearest_by_levenshtein(compared_string = nil, column = 'verbatim_locality', limit = 10) ⇒ Scope
DEPRECATED for shared code
707 708 709 710 711 712 713 |
# File 'app/models/collecting_event.rb', line 707 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 |
#set_cached ⇒ Object (protected)
1110 1111 1112 1113 |
# File 'app/models/collecting_event.rb', line 1110 def set_cached set_cached_geographic_names # if saved_change_to_attribute?(:geographic_area_id) set_cached_cached end |
#set_cached_cached ⇒ Object (protected)
TODO: A time sync.
1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 |
# File 'app/models/collecting_event.rb', line 1116 def set_cached_cached c = {} v = [verbatim_label, print_label, document_label].compact.first if v string = v else name = [ geographic_names[:country], geographic_names[:state], geographic_names[:county]].compact.join(': ') date = [start_date_string, end_date_string].compact.join('-') place_date = [verbatim_locality, date].compact.join(', ') # TODO: When a method to reference Collector roles is created update Collector to trigger this cached method string = [name, place_date, verbatim_collectors, verbatim_method].reject{|a| a.blank? }.join("\n") end string = "[#{id}]" if string.blank? c[:cached] = string update_columns(c) end |
#set_cached_geographic_names ⇒ Object (protected)
1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 |
# File 'app/models/collecting_event.rb', line 1087 def set_cached_geographic_names # prevent a second call to get if we've already tried through values = get_geographic_name_classification if values.empty? update_columns( cached_level0_geographic_name: nil, cached_level1_geographic_name: nil, cached_level2_geographic_name: nil) @geographic_names = {} else update_columns( cached_level0_geographic_name: values[:country], cached_level1_geographic_name: values[:state], cached_level2_geographic_name: values[:county]) # Update attribute to ensure it can be used post save @geographic_names = values @geographic_names.delete_if{|k,v| v.nil?} @geographic_names end end |
#set_times_to_nil_if_form_provided_blank ⇒ Object (protected)
1137 1138 1139 1140 1141 |
# File 'app/models/collecting_event.rb', line 1137 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 |
#similar_lat_longs(lat, long, project_id, piece = '', include_values = true) ⇒ Scope
Returns of matching collecting events TODO: deprecate and move to filter.
448 449 450 451 452 453 454 455 456 457 458 459 460 461 |
# File 'app/models/collecting_event.rb', line 448 def similar_lat_longs(lat, long, project_id, piece = '', include_values = true) sql = '(' sql += "verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(lat)}%'" if lat.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(long)}%'" if long.present? sql += " or verbatim_label LIKE '%#{::Utilities::Strings.escape_single_quote(piece)}%'" if piece.present? 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:).distinct retval end |
#some_end_date? ⇒ Boolean
498 499 500 |
# File 'app/models/collecting_event.rb', line 498 def some_end_date? [end_date_day, end_date_month, end_date_year].compact.any? end |
#some_start_date? ⇒ Boolean
494 495 496 |
# File 'app/models/collecting_event.rb', line 494 def some_start_date? [start_date_day, start_date_month, start_date_year].compact.any? end |
#start_date ⇒ Time
Returns This is for the purposes of computation, not display!.
540 541 542 |
# File 'app/models/collecting_event.rb', line 540 def start_date Utilities::Dates.nomenclature_date(start_date_day, start_date_month, start_date_year) end |
#start_date_string ⇒ String?
Returns an umabigously formatted string with missing parts indicated by ??.
522 523 524 |
# File 'app/models/collecting_event.rb', line 522 def start_date_string Utilities::Dates.from_parts(start_date_year, start_date_month, start_date_day) if some_start_date? end |
#start_day_of_year ⇒ Integer?
Returns the start day of the event from 1-365 (or 366 in leap years) only returned when completely unambigouous, in theory could be added for month/day combinations on, but that is uncertain.
506 507 508 509 |
# File 'app/models/collecting_event.rb', line 506 def start_day_of_year return unless has_start_date? Date.new(start_date_year, start_date_month, start_date_day).yday end |
#state_name ⇒ String
891 892 893 |
# File 'app/models/collecting_event.rb', line 891 def state_name state_or_province_name end |
#state_or_province_name ⇒ String
886 887 888 |
# File 'app/models/collecting_event.rb', line 886 def state_or_province_name name_from_geopolitical_hash(states_hash) end |
#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
851 852 853 |
# File 'app/models/collecting_event.rb', line 851 def states_hash name_hash(GeographicAreaType::STATE_LEVEL_TYPES) end |
#sv_georeference_matches_verbatim ⇒ Object (protected)
1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 |
# File 'app/models/collecting_event.rb', line 1183 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 |
#sv_minimally_check_for_a_label ⇒ Object (protected)
1204 1205 1206 1207 1208 1209 |
# File 'app/models/collecting_event.rb', line 1204 def sv_minimally_check_for_a_label [:verbatim_label, :print_label, :document_label, :field_notes].each do |v| return true if self.send(v).present? end soft_validations.add(:base, 'At least one label type, or field notes, should be provided.') end |
#sv_missing_geographic_area ⇒ Object (protected)
1200 1201 1202 |
# File 'app/models/collecting_event.rb', line 1200 def sv_missing_geographic_area soft_validations.add(:geographic_area_id, 'Geographic area is missing') if geographic_area_id.nil? && !georeferences.any? end |
#sv_missing_georeference ⇒ Object (protected)
1179 1180 1181 |
# File 'app/models/collecting_event.rb', line 1179 def sv_missing_georeference soft_validations.add(:base, 'Georeference is missing') unless georeferences.any? end |
#sv_verbatim_uncertainty_format ⇒ Object (protected)
1211 1212 1213 1214 1215 1216 1217 1218 1219 |
# File 'app/models/collecting_event.rb', line 1211 def sv_verbatim_uncertainty_format begin if verbatim_geolocation_uncertainty.present? && verbatim_geolocation_uncertainty.to_unit return true end rescue ArgumentError soft_validations.add(:verbatim_geolocation_uncertainty, 'appears to be malformed, has no units?') end end |
#time_end ⇒ String
Returns like 00, 00:00, or 00:00:00.
552 553 554 |
# File 'app/models/collecting_event.rb', line 552 def time_end Utilities::Dates.format_to_hours_minutes_seconds(time_end_hour, time_end_minute, time_end_second) end |
#time_range ⇒ Array
Returns time_start and end if provided.
558 559 560 |
# File 'app/models/collecting_event.rb', line 558 def time_range [time_start, time_end].compact end |
#time_start ⇒ String
Returns like 00, 00:00, or 00:00:00.
546 547 548 |
# File 'app/models/collecting_event.rb', line 546 def time_start Utilities::Dates.format_to_hours_minutes_seconds(time_start_hour, time_start_minute, time_start_second) end |
#to_geo_json_feature ⇒ GeoJSON::Feature
TODO: DRY with helper methods, these are now outdated approaches
905 906 907 908 909 910 911 912 913 914 915 916 |
# File 'app/models/collecting_event.rb', line 905 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 |
#to_simple_json_feature ⇒ Object
TODO: DRY with helper methods, these are now outdated approaches
i.e. geographic_areas_geogrpahic_items.where( gaz = 'some string')
920 921 922 923 924 925 926 927 928 929 930 931 932 |
# File 'app/models/collecting_event.rb', line 920 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 |
#verbatim_map_center(delta_z = 0.0) ⇒ RGeo::Geographic::ProjectedPointImpl?
Returns for the verbatim latitude/longitude only.
938 939 940 941 942 943 944 945 946 947 948 |
# File 'app/models/collecting_event.rb', line 938 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 |