001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 *     http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014package org.gbif.api.vocabulary;
015
016import org.gbif.api.util.AnnotationUtils;
017import org.gbif.dwc.terms.DcTerm;
018import org.gbif.dwc.terms.DwcTerm;
019import org.gbif.dwc.terms.GbifDnaTerm;
020import org.gbif.dwc.terms.MixsTerm;
021import org.gbif.dwc.terms.Term;
022
023import java.util.Arrays;
024import java.util.Collections;
025import java.util.HashSet;
026import java.util.List;
027import java.util.Set;
028import java.util.stream.Collectors;
029
030import org.apache.commons.lang3.ArrayUtils;
031
032import static org.gbif.api.vocabulary.InterpretationRemarkSeverity.ERROR;
033import static org.gbif.api.vocabulary.InterpretationRemarkSeverity.INFO;
034import static org.gbif.api.vocabulary.InterpretationRemarkSeverity.WARNING;
035
036/** An enumeration of validation rules for single occurrence records. */
037public enum OccurrenceIssue implements InterpretationRemark {
038
039  /** Coordinate is the exact 0°, 0° coordinate, often indicating a bad null coordinate. */
040  ZERO_COORDINATE(WARNING, TermsGroup.COORDINATES_TERMS_NO_DATUM),
041
042  /**
043   * Coordinate has a latitude and/or longitude value beyond the maximum (or minimum) decimal value.
044   */
045  COORDINATE_OUT_OF_RANGE(WARNING, TermsGroup.COORDINATES_TERMS_NO_DATUM),
046
047  /** Coordinate value is given in some form but GBIF is unable to interpret it. */
048  COORDINATE_INVALID(WARNING, TermsGroup.COORDINATES_TERMS_NO_DATUM),
049
050  /** Original coordinate modified by rounding to 5 decimals. */
051  COORDINATE_ROUNDED(INFO, TermsGroup.COORDINATES_TERMS_NO_DATUM),
052
053  /** The geodetic datum given could not be interpreted. */
054  GEODETIC_DATUM_INVALID(WARNING, DwcTerm.geodeticDatum),
055
056  /**
057   * Indicating that the interpreted coordinates assume they are based on WGS84 datum as the datum
058   * was either not indicated or interpretable. See GEODETIC_DATUM_INVALID.
059   */
060  GEODETIC_DATUM_ASSUMED_WGS84(INFO, DwcTerm.geodeticDatum),
061
062  /**
063   * The original coordinate was successfully reprojected from a different geodetic datum to WGS84.
064   */
065  COORDINATE_REPROJECTED(INFO, TermsGroup.COORDINATES_TERMS),
066
067  /**
068   * The given decimal latitude and longitude could not be reprojected to WGS84 based on the
069   * provided datum.
070   */
071  COORDINATE_REPROJECTION_FAILED(WARNING, TermsGroup.COORDINATES_TERMS),
072
073  /**
074   * Indicates successful coordinate reprojection according to provided datum, but which results in
075   * a datum shift larger than 0.1 decimal degrees.
076   */
077  COORDINATE_REPROJECTION_SUSPICIOUS(WARNING, TermsGroup.COORDINATES_TERMS),
078
079  /**
080   * Indicates an invalid or very unlikely coordinate accuracy derived from precision or uncertainty
081   * in meters.
082   */
083  @Deprecated // see POR-3061
084  COORDINATE_ACCURACY_INVALID(WARNING),
085
086  /** Indicates an invalid or very unlikely coordinatePrecision */
087  COORDINATE_PRECISION_INVALID(WARNING, DwcTerm.coordinatePrecision),
088
089  /** Indicates an invalid or very unlikely dwc:uncertaintyInMeters. */
090  COORDINATE_UNCERTAINTY_METERS_INVALID(WARNING, DwcTerm.coordinateUncertaintyInMeters),
091
092  /** There is a mismatch between coordinate uncertainty in meters and coordinate precision. */
093  @Deprecated // see POR-1804
094  COORDINATE_PRECISION_UNCERTAINTY_MISMATCH(WARNING),
095
096  /** The Footprint Spatial Reference System given could not be interpreted. */
097  FOOTPRINT_SRS_INVALID(WARNING, DwcTerm.footprintSRS),
098
099  /**
100   * The Footprint Well-Known-Text conflicts with the interpreted coordinates (Decimal Latitude,
101   * Decimal Longitude etc).
102   */
103  FOOTPRINT_WKT_MISMATCH(WARNING, DwcTerm.footprintWKT),
104
105  /** The Footprint Well-Known-Text given could not be interpreted. */
106  FOOTPRINT_WKT_INVALID(WARNING, DwcTerm.footprintWKT),
107
108  /** The interpreted occurrence coordinates fall outside of the indicated country. */
109  COUNTRY_COORDINATE_MISMATCH(WARNING, TermsGroup.COORDINATES_COUNTRY_TERMS),
110
111  /** Interpreted country for dwc:country and dwc:countryCode contradict each other. */
112  COUNTRY_MISMATCH(WARNING, TermsGroup.COUNTRY_TERMS),
113
114  /** Uninterpretable country values found. */
115  COUNTRY_INVALID(WARNING, TermsGroup.COUNTRY_TERMS),
116
117  /** The interpreted country is based on the coordinates, not the verbatim string information. */
118  COUNTRY_DERIVED_FROM_COORDINATES(WARNING, TermsGroup.COORDINATES_COUNTRY_TERMS),
119
120  /** The interpreted occurrence coordinates fall outside of the indicated continent. */
121  CONTINENT_COORDINATE_MISMATCH(WARNING),
122
123  /** The interpreted continent and country do not match. */
124  CONTINENT_COUNTRY_MISMATCH(WARNING),
125
126  /** Uninterpretable continent values found. */
127  CONTINENT_INVALID(WARNING),
128
129  /** The interpreted continent is based on the country, not the verbatim string information. */
130  CONTINENT_DERIVED_FROM_COUNTRY(WARNING),
131
132  /** The interpreted continent is based on the coordinates, not the verbatim string information. */
133  CONTINENT_DERIVED_FROM_COORDINATES(WARNING),
134
135  /** Latitude and longitude appear to be swapped. */
136  PRESUMED_SWAPPED_COORDINATE(WARNING, TermsGroup.COORDINATES_TERMS_NO_DATUM),
137
138  /** Longitude appears to be negated, e.g. 32.3 instead of -32.3 */
139  PRESUMED_NEGATED_LONGITUDE(WARNING, TermsGroup.COORDINATES_TERMS_NO_DATUM),
140
141  /** Latitude appears to be negated, e.g. 32.3 instead of -32.3 */
142  PRESUMED_NEGATED_LATITUDE(WARNING, TermsGroup.COORDINATES_TERMS_NO_DATUM),
143
144  /** Latitude and longitude suspected to be swapped. */
145  SUSPECTED_SWAPPED_COORDINATE(WARNING, TermsGroup.COORDINATES_TERMS_NO_DATUM),
146
147  /** Longitude suspected to be negated, e.g. 32.3 instead of -32.3 */
148  SUSPECTED_NEGATED_LONGITUDE(WARNING, TermsGroup.COORDINATES_TERMS_NO_DATUM),
149
150  /** Latitude suspected to be negated, e.g. 32.3 instead of -32.3 */
151  SUSPECTED_NEGATED_LATITUDE(WARNING, TermsGroup.COORDINATES_TERMS_NO_DATUM),
152
153  /**
154   * The recorded date specified as the eventDate string and the individual year, month, day and/or
155   * startDayOfYear, endDayOfYear are contradictory.
156   */
157  RECORDED_DATE_MISMATCH(WARNING, TermsGroup.RECORDED_DATE_TERMS),
158
159  /** A (partial) invalid date is given, such as a non-existent date, zero month, etc. */
160  RECORDED_DATE_INVALID(WARNING, TermsGroup.RECORDED_DATE_TERMS),
161
162  /**
163   * The recorded date is highly unlikely, falling either into the future or representing a very old
164   * date before 1600 thus predating modern taxonomy.
165   */
166  RECORDED_DATE_UNLIKELY(WARNING, TermsGroup.RECORDED_DATE_TERMS),
167
168  /** Matching to the taxonomic backbone can only be done using a fuzzy, non-exact match. */
169  TAXON_MATCH_FUZZY(WARNING, TermsGroup.TAXONOMY_TERMS),
170
171  /**
172   * Matching to the taxonomic backbone can only be done on a higher rank and not the scientific
173   * name.
174   */
175  TAXON_MATCH_HIGHERRANK(WARNING, TermsGroup.TAXONOMY_TERMS),
176
177  /**
178   * Matching to the taxonomic backbone can only be done on a species level, but the occurrence was
179   * in fact considered a broader species aggregate/complex.
180   *
181   * @see <a
182   *     href="https://github.com/gbif/portal-feedback/issues/2935">gbif/portal-feedback#2935</a>
183   */
184  TAXON_MATCH_AGGREGATE(WARNING, TermsGroup.TAXONOMY_TERMS),
185
186  /**
187   * The scientificNameID was not used when mapping the record to the taxonomy. This may
188   * indicate one of
189   *
190   * <ul>
191   *   <li>The ID uses a pattern not configured for use by GBIF
192   *   <li>The ID did not uniquely(!) identify a concept in the checklist
193   *   <li>The ID found a concept in the checklist which did not map to the backbone
194   *   <li>A different ID was used, or the record names were used as no ID lookup successfully
195   *       linked to the backbone
196   * </ul>
197   *
198   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
199   */
200  TAXON_MATCH_SCIENTIFIC_NAME_ID_IGNORED(INFO, DwcTerm.scientificNameID),
201
202  /**
203   * The taxonConceptID was not used when mapping the record to the taxonomy. This may indicate
204   * one of
205   *
206   * <ul>
207   *   <li>The ID uses a pattern not configured for use by GBIF
208   *   <li>The ID did not uniquely(!) identify a concept in the checklist
209   *   <li>The ID found a concept in the checklist which did not map to the backbone
210   *   <li>A different ID was used, or the record names were used as no ID lookup successfully
211   *       linked to the backbone
212   * </ul>
213   *
214   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
215   */
216  TAXON_MATCH_TAXON_CONCEPT_ID_IGNORED(INFO, DwcTerm.taxonConceptID),
217
218  /**
219   * The taxonID was not used when mapping the record to the taxonomy. This may indicate one of
220   *
221   * <ul>
222   *   <li>The ID uses a pattern not configured for use by GBIF
223   *   <li>The ID did not uniquely(!) identify a concept in the checklist
224   *   <li>The ID found a concept in the checklist which did not map to the backbone
225   *   <li>A different ID was used, or the record names were used as no ID lookup successfully
226   *       linked to the backbone
227   * </ul>
228   *
229   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
230   */
231  TAXON_MATCH_TAXON_ID_IGNORED(INFO, DwcTerm.taxonID),
232
233  /**
234   * The scientificNameID matched a known pattern, but it was not found in the associated checklist.
235   * The backbone lookup was performed using either the names or a different ID on the record. This
236   * may indicate a poorly formatted identifier or may be caused by a newly created ID that isn't
237   * yet known in the version of the published checklist.
238   *
239   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
240   */
241  SCIENTIFIC_NAME_ID_NOT_FOUND(WARNING, DwcTerm.scientificNameID),
242
243  /**
244   * The taxonConceptID matched a known pattern, but it was not found in the associated checklist.
245   * The backbone lookup was performed using either the names or a different ID on the record. This
246   * may indicate a poorly formatted identifier or may be caused by a newly created ID that isn't
247   * yet known in the version of the published checklist.
248   *
249   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
250   */
251  TAXON_CONCEPT_ID_NOT_FOUND(WARNING, DwcTerm.taxonConceptID),
252
253  /**
254   * The taxonID matched a known pattern, but it was not found in the associated checklist. The
255   * backbone lookup was performed using either the names or a different ID on the record. This may
256   * indicate a poorly formatted identifier or may be caused by a newly created ID that isn't yet
257   * known in the version of the published checklist.
258   *
259   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
260   */
261  TAXON_ID_NOT_FOUND(WARNING, DwcTerm.taxonID),
262
263  /**
264   * The scientificName provided in the occurrence record does not precisely match the name in the
265   * registered checklist when using the scientificNameID, taxonID or taxonConceptID to look it up.
266   * Publishers are advised to check the IDs are correct, or update the formatting of the names on
267   * their records.
268   *
269   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
270   */
271  SCIENTIFIC_NAME_AND_ID_INCONSISTENT(
272      WARNING,
273      DwcTerm.scientificNameID,
274      DwcTerm.taxonID,
275      DwcTerm.taxonConceptID,
276      DwcTerm.scientificName),
277
278  /**
279   * Matching to the taxonomic backbone cannot be done because there was no match at all, or several
280   * matches with too little information to keep them apart (potentially homonyms).
281   */
282  TAXON_MATCH_NONE(WARNING, TermsGroup.TAXONOMY_TERMS),
283
284  /**
285   * The taxon concept was found using the scientificNameID, taxonID or taxonConceptID, but
286   * it differs from what would have been found if the classification names on the record were used.
287   * This may indicate a gap in the taxonomy, a poor mapping between the checklist and the
288   * backbone, or a mismatch between the classification names and the declared IDs (scientificNameID
289   * or taxonConceptID) on the occurrence record itself.
290   *
291   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
292   */
293  TAXON_MATCH_NAME_AND_ID_AMBIGUOUS(WARNING, TermsGroup.TAXONOMY_TERMS),
294
295  /**
296   * Set if supplied depth is not given in the metric system, for example using feet instead of
297   * meters
298   */
299  DEPTH_NOT_METRIC(WARNING, DwcTerm.minimumDepthInMeters, DwcTerm.maximumDepthInMeters),
300
301  /** Set if depth is larger than 11,000m or negative. */
302  DEPTH_UNLIKELY(WARNING, DwcTerm.minimumDepthInMeters, DwcTerm.maximumDepthInMeters),
303
304  /** Set if supplied minimum depth > maximum depth */
305  DEPTH_MIN_MAX_SWAPPED(WARNING, DwcTerm.minimumDepthInMeters, DwcTerm.maximumDepthInMeters),
306
307  /** Set if depth is a non-numeric value */
308  DEPTH_NON_NUMERIC(WARNING, DwcTerm.minimumDepthInMeters, DwcTerm.maximumDepthInMeters),
309
310  /** Set if elevation is above the troposphere (17km) or below 11km (Mariana Trench). */
311  ELEVATION_UNLIKELY(WARNING, DwcTerm.minimumElevationInMeters, DwcTerm.maximumElevationInMeters),
312
313  /** Set if supplied minimum elevation > maximum elevation */
314  ELEVATION_MIN_MAX_SWAPPED(
315      WARNING, DwcTerm.minimumElevationInMeters, DwcTerm.maximumElevationInMeters),
316
317  /**
318   * Set if supplied elevation is not given in the metric system, for example using feet instead of
319   * meters
320   */
321  ELEVATION_NOT_METRIC(WARNING, DwcTerm.minimumElevationInMeters, DwcTerm.maximumElevationInMeters),
322
323  /** Set if elevation is a non-numeric value */
324  ELEVATION_NON_NUMERIC(
325      WARNING, DwcTerm.minimumElevationInMeters, DwcTerm.maximumElevationInMeters),
326
327  /**
328   * A (partial) invalid date is given for dc:modified, such as a nonexistent date, zero month, etc.
329   */
330  MODIFIED_DATE_INVALID(WARNING, DcTerm.modified),
331
332  /** The date given for dc:modified is in the future or predates Unix time (1970). */
333  MODIFIED_DATE_UNLIKELY(WARNING, DcTerm.modified),
334
335  /** The date given for dwc:dateIdentified is in the future or before Linnean times (1700). */
336  IDENTIFIED_DATE_UNLIKELY(WARNING, DwcTerm.dateIdentified),
337
338  /** The date given for dwc:dateIdentified is invalid and can't be interpreted at all. */
339  IDENTIFIED_DATE_INVALID(WARNING, DwcTerm.dateIdentified),
340
341  /**
342   * The given basis of record is impossible to interpret or significantly different from the
343   * recommended vocabulary.
344   */
345  BASIS_OF_RECORD_INVALID(WARNING, DwcTerm.basisOfRecord),
346
347  /**
348   * The given type status is impossible to interpret or significantly different from the
349   * recommended vocabulary.
350   */
351  TYPE_STATUS_INVALID(WARNING, DwcTerm.typeStatus),
352
353  /** The given type status contains some words that express uncertainty. */
354  SUSPECTED_TYPE(WARNING, DwcTerm.typeStatus),
355
356  /** An invalid date is given for dc:created of a multimedia object. */
357  MULTIMEDIA_DATE_INVALID(WARNING),
358
359  /** An invalid URI is given for a multimedia object. */
360  MULTIMEDIA_URI_INVALID(WARNING),
361
362  /** An invalid URI is given for dc:references. */
363  REFERENCES_URI_INVALID(WARNING, DcTerm.references),
364
365  /** An error occurred during interpretation, leaving the record interpretation incomplete. */
366  INTERPRETATION_ERROR(ERROR),
367
368  /** The individual count value is not a positive integer */
369  INDIVIDUAL_COUNT_INVALID(WARNING, DwcTerm.individualCount),
370
371  /** Example: individual count value > 0, but occurrence status is absent. */
372  INDIVIDUAL_COUNT_CONFLICTS_WITH_OCCURRENCE_STATUS(WARNING, DwcTerm.individualCount),
373
374  /** Occurrence status value can't be assigned to {@link OccurrenceStatus} */
375  OCCURRENCE_STATUS_UNPARSABLE(WARNING, DwcTerm.occurrenceStatus),
376
377  /** Occurrence status was inferred from the individual count value */
378  OCCURRENCE_STATUS_INFERRED_FROM_INDIVIDUAL_COUNT(WARNING, DwcTerm.occurrenceStatus),
379
380  /** Occurrence status was inferred from basis of records */
381  OCCURRENCE_STATUS_INFERRED_FROM_BASIS_OF_RECORD(WARNING, DwcTerm.occurrenceStatus),
382
383  /** The date given for dwc:georeferencedDate is in the future or before Linnean times (1700). */
384  GEOREFERENCED_DATE_UNLIKELY(WARNING, DwcTerm.georeferencedDate),
385
386  /** The date given for dwc:georeferencedDate is invalid and can't be interpreted at all. */
387  GEOREFERENCED_DATE_INVALID(WARNING, DwcTerm.georeferencedDate),
388
389  /** The given institution matches with more than 1 GRSciColl institution. */
390  AMBIGUOUS_INSTITUTION(INFO, TermsGroup.INSTITUTION_TERMS),
391
392  /** The given collection matches with more than 1 GRSciColl collection. */
393  AMBIGUOUS_COLLECTION(INFO, TermsGroup.COLLECTION_TERMS),
394
395  /** The given institution couldn't be matched with any GRSciColl institution. */
396  INSTITUTION_MATCH_NONE(INFO, TermsGroup.INSTITUTION_TERMS),
397
398  /** The given collection couldn't be matched with any GRSciColl collection. */
399  COLLECTION_MATCH_NONE(INFO, TermsGroup.COLLECTION_TERMS),
400
401  /**
402   * The given institution was fuzzily matched to a GRSciColl institution. This can happen when
403   * either the code or the ID don't match or when the institution name is used instead of the code.
404   */
405  INSTITUTION_MATCH_FUZZY(INFO, TermsGroup.INSTITUTION_TERMS),
406
407  /**
408   * The given collection was fuzzily matched to a GRSciColl collection. This can happen when either
409   * the code or the ID don't match or when the collection name is used instead of the code.
410   */
411  COLLECTION_MATCH_FUZZY(INFO, TermsGroup.COLLECTION_TERMS),
412
413  /** The collection matched doesn't belong to the institution matched. */
414  INSTITUTION_COLLECTION_MISMATCH(
415      INFO, ArrayUtils.addAll(TermsGroup.INSTITUTION_TERMS, TermsGroup.INSTITUTION_TERMS)),
416
417  /**
418   * The given owner institution is different than the given institution. Therefore we assume it
419   * could be on loan and we don't link it to the occurrence.
420   *
421   * <p>Deprecated by {@link #DIFFERENT_OWNER_INSTITUTION}.
422   */
423  @Deprecated
424  POSSIBLY_ON_LOAN(INFO, TermsGroup.INSTITUTION_TERMS),
425
426  /**
427   * The given owner institution is different than the given institution. Therefore we assume it
428   * doesn't belong to the institution and we don't link it to the occurrence.
429   */
430  DIFFERENT_OWNER_INSTITUTION(INFO, TermsGroup.INSTITUTION_TERMS),
431
432  /** Era or erathem was inferred from a parent rank. */
433  ERA_OR_ERATHEM_INFERRED_FROM_PARENT_RANK(
434      INFO, DwcTerm.earliestEraOrLowestErathem, DwcTerm.latestEraOrHighestErathem),
435  /** Period or system was inferred from a parent rank. */
436  PERIOD_OR_SYSTEM_INFERRED_FROM_PARENT_RANK(
437      INFO, DwcTerm.earliestPeriodOrLowestSystem, DwcTerm.latestPeriodOrHighestSystem),
438  /** Epoch or series was inferred from a parent rank. */
439  EPOCH_OR_SERIES_INFERRED_FROM_PARENT_RANK(
440      INFO, DwcTerm.earliestEpochOrLowestSeries, DwcTerm.latestEpochOrHighestSeries),
441  /** Age or stage was inferred from a parent rank. */
442  AGE_OR_STAGE_INFERRED_FROM_PARENT_RANK(
443      INFO, DwcTerm.earliestAgeOrLowestStage, DwcTerm.latestAgeOrHighestStage),
444
445  /** The eon or eonothem provided belongs to another rank. */
446  EON_OR_EONOTHEM_RANK_MISMATCH(
447      INFO, DwcTerm.earliestEonOrLowestEonothem, DwcTerm.latestEonOrHighestEonothem),
448  /** The era or erathem provided belongs to another rank. */
449  ERA_OR_ERATHEM_RANK_MISMATCH(
450      INFO, DwcTerm.earliestEraOrLowestErathem, DwcTerm.latestEraOrHighestErathem),
451  /** The period or system provided belongs to another rank. */
452  PERIOD_OR_SYSTEM_RANK_MISMATCH(
453      INFO, DwcTerm.earliestPeriodOrLowestSystem, DwcTerm.latestPeriodOrHighestSystem),
454  /** The period or series provided belongs to another rank. */
455  EPOCH_OR_SERIES_RANK_MISMATCH(
456      INFO, DwcTerm.earliestEpochOrLowestSeries, DwcTerm.latestEpochOrHighestSeries),
457  /** The age or stage provided belongs to another rank. */
458  AGE_OR_STAGE_RANK_MISMATCH(
459      INFO, DwcTerm.earliestAgeOrLowestStage, DwcTerm.latestAgeOrHighestStage),
460
461  /** The earliest eon or eonothem has to be earlier than the latest. */
462  EON_OR_EONOTHEM_INVALID_RANGE(
463      INFO, DwcTerm.earliestEonOrLowestEonothem, DwcTerm.latestEonOrHighestEonothem),
464  /** The era or erathem has to be earlier than the latest. */
465  ERA_OR_ERATHEM_INVALID_RANGE(
466      INFO, DwcTerm.earliestEraOrLowestErathem, DwcTerm.latestEraOrHighestErathem),
467  /** The period or system has to be earlier than the latest. */
468  PERIOD_OR_SYSTEM_INVALID_RANGE(
469      INFO, DwcTerm.earliestPeriodOrLowestSystem, DwcTerm.latestPeriodOrHighestSystem),
470  /** The period or series has to be earlier than the latest. */
471  EPOCH_OR_SERIES_INVALID_RANGE(
472      INFO, DwcTerm.earliestEpochOrLowestSeries, DwcTerm.latestEpochOrHighestSeries),
473  /** The age or stage has to be earlier than the latest. */
474  AGE_OR_STAGE_INVALID_RANGE(
475      INFO, DwcTerm.earliestAgeOrLowestStage, DwcTerm.latestAgeOrHighestStage),
476
477  /** The era or erathem don't belong to the eon or eonothem. */
478  EON_OR_EONOTHEM_AND_ERA_OR_ERATHEM_MISMATCH(
479      INFO,
480      DwcTerm.earliestEonOrLowestEonothem,
481      DwcTerm.latestEonOrHighestEonothem,
482      DwcTerm.earliestEraOrLowestErathem,
483      DwcTerm.latestEraOrHighestErathem),
484
485  /** The period or system don't belong to the era or erathem. */
486  ERA_OR_ERATHEM_AND_PERIOD_OR_SYSTEM_MISMATCH(
487      INFO,
488      DwcTerm.earliestEraOrLowestErathem,
489      DwcTerm.latestEraOrHighestErathem,
490      DwcTerm.earliestPeriodOrLowestSystem,
491      DwcTerm.latestPeriodOrHighestSystem),
492
493  /** The epoch or series don't belong to the period or system. */
494  PERIOD_OR_SYSTEM_AND_EPOCH_OR_SERIES_MISMATCH(
495      INFO,
496      DwcTerm.earliestPeriodOrLowestSystem,
497      DwcTerm.latestPeriodOrHighestSystem,
498      DwcTerm.earliestEpochOrLowestSeries,
499      DwcTerm.latestEpochOrHighestSeries),
500
501  /** The age or stage don't belong to the epoch or series. */
502  EPOCH_OR_SERIES_AND_AGE_OR_STAGE_MISMATCH(
503      INFO,
504      DwcTerm.earliestEpochOrLowestSeries,
505      DwcTerm.latestEpochOrHighestSeries,
506      DwcTerm.earliestAgeOrLowestStage,
507      DwcTerm.latestAgeOrHighestStage),
508
509  /** Set when natural language text is detected in {@code dna_sequence}. */
510  NUCLEOTIDE_SEQUENCE_NATURAL_LANGUAGE(ERROR, GbifDnaTerm.dna_sequence),
511
512  /** Set when leading or trailing sequence characters are trimmed. */
513  NUCLEOTIDE_SEQUENCE_ENDS_TRIMMED(INFO, GbifDnaTerm.dna_sequence),
514
515  /** Set when gaps or whitespace are removed from the sequence. */
516  NUCLEOTIDE_SEQUENCE_GAPS_REMOVED(INFO, GbifDnaTerm.dna_sequence),
517
518  /** Set when the sequence is invalid after normalization and validation. */
519  NUCLEOTIDE_SEQUENCE_INVALID(ERROR, GbifDnaTerm.dna_sequence),
520
521  /** Set when the fraction of N bases exceeds the configured threshold. */
522  NUCLEOTIDE_SEQUENCE_HIGH_N_FRACTION(WARNING, GbifDnaTerm.dna_sequence),
523
524  /** Set when the fraction of non-ACGTN bases exceeds the configured threshold. */
525  NUCLEOTIDE_SEQUENCE_HIGH_AMBIGUITY(WARNING, GbifDnaTerm.dna_sequence),
526
527  /** Set when {@code target_gene} does not resolve to a concept in the vocabulary. */
528  TARGET_GENE_INVALID(WARNING, MixsTerm.target_gene),
529
530  /** Indicates that duplicate nucleotide sequences have been found and collapsed. */
531  DUPLICATE_NUCLEOTIDE_SEQUENCES_COLLAPSED(WARNING, GbifDnaTerm.dna_sequence, MixsTerm.target_gene),
532
533  /**
534   * Set when an infinite loop is detected in the event lineage.
535   */
536  PARENT_EVENT_INFINITE_LINEAGE(INFO, DwcTerm.parentEventID);
537
538  /**
539   * Simple helper nested class to allow grouping of Term mostly to increase readability of this
540   * class.
541   */
542  private static class TermsGroup {
543
544    static final Term[] COORDINATES_TERMS_NO_DATUM = {
545      DwcTerm.decimalLatitude,
546      DwcTerm.decimalLongitude,
547      DwcTerm.verbatimLatitude,
548      DwcTerm.verbatimLongitude,
549      DwcTerm.verbatimCoordinates
550    };
551
552    static final Term[] COORDINATES_TERMS = {
553      DwcTerm.decimalLatitude,
554      DwcTerm.decimalLongitude,
555      DwcTerm.verbatimLatitude,
556      DwcTerm.verbatimLongitude,
557      DwcTerm.verbatimCoordinates,
558      DwcTerm.geodeticDatum
559    };
560
561    static final Term[] COUNTRY_TERMS = {DwcTerm.country, DwcTerm.countryCode};
562
563    static final Term[] COORDINATES_COUNTRY_TERMS = {
564      DwcTerm.decimalLatitude,
565      DwcTerm.decimalLongitude,
566      DwcTerm.verbatimLatitude,
567      DwcTerm.verbatimLongitude,
568      DwcTerm.verbatimCoordinates,
569      DwcTerm.geodeticDatum,
570      DwcTerm.country,
571      DwcTerm.countryCode
572    };
573
574    static final Term[] RECORDED_DATE_TERMS = {
575      DwcTerm.eventDate,
576      DwcTerm.year,
577      DwcTerm.month,
578      DwcTerm.day,
579      DwcTerm.startDayOfYear,
580      DwcTerm.endDayOfYear
581    };
582
583    static final Term[] TAXONOMY_TERMS = {
584      DwcTerm.kingdom,
585      DwcTerm.phylum,
586      DwcTerm.class_,
587      DwcTerm.order,
588      DwcTerm.family,
589      DwcTerm.genus,
590      DwcTerm.scientificName,
591      DwcTerm.scientificNameAuthorship,
592      DwcTerm.genericName,
593      DwcTerm.specificEpithet,
594      DwcTerm.infraspecificEpithet,
595      DwcTerm.scientificNameID,
596      DwcTerm.taxonConceptID,
597      DwcTerm.taxonID,
598    };
599
600    static final Term[] INSTITUTION_TERMS = {
601      DwcTerm.institutionCode, DwcTerm.institutionID, DwcTerm.ownerInstitutionCode
602    };
603
604    static final Term[] COLLECTION_TERMS = {DwcTerm.collectionCode, DwcTerm.collectionID};
605  }
606
607  private final Set<Term> relatedTerms;
608  private final InterpretationRemarkSeverity severity;
609  private final boolean isDeprecated;
610
611  /** {@link OccurrenceIssue} not linked to any specific {@link Term}. */
612  OccurrenceIssue(InterpretationRemarkSeverity severity) {
613    this.severity = severity;
614    this.relatedTerms = Collections.emptySet();
615    this.isDeprecated = AnnotationUtils.isFieldDeprecated(OccurrenceIssue.class, this.name());
616  }
617
618  /** {@link OccurrenceIssue} linked to the provided {@link Term}. */
619  OccurrenceIssue(InterpretationRemarkSeverity severity, Term... relatedTerms) {
620    this.severity = severity;
621    this.relatedTerms = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(relatedTerms)));
622    this.isDeprecated = AnnotationUtils.isFieldDeprecated(OccurrenceIssue.class, this.name());
623  }
624
625  @Override
626  public String getId() {
627    return name();
628  }
629
630  @Override
631  public Set<Term> getRelatedTerms() {
632    return relatedTerms;
633  }
634
635  @Override
636  public InterpretationRemarkSeverity getSeverity() {
637    return severity;
638  }
639
640  @Override
641  public boolean isDeprecated() {
642    return isDeprecated;
643  }
644
645  /**
646   * All issues that indicate problems with the coordinates and thus should not be shown on maps.
647   */
648  public static final List<OccurrenceIssue> GEOSPATIAL_RULES =
649      Collections.unmodifiableList(
650          Arrays.asList(
651              ZERO_COORDINATE,
652              COORDINATE_OUT_OF_RANGE,
653              COORDINATE_INVALID,
654              COUNTRY_COORDINATE_MISMATCH,
655              PRESUMED_SWAPPED_COORDINATE,
656              PRESUMED_NEGATED_LONGITUDE,
657              PRESUMED_NEGATED_LATITUDE,
658              SUSPECTED_SWAPPED_COORDINATE,
659              SUSPECTED_NEGATED_LONGITUDE,
660              SUSPECTED_NEGATED_LATITUDE));
661
662  /** All issues related to taxonomic fields. */
663  public static final List<OccurrenceIssue> TAXONOMIC_RULES =
664      Set.of(OccurrenceIssue.values()).stream()
665          .filter(
666              issue ->
667                  issue.getRelatedTerms().stream()
668                      .anyMatch(term -> Set.of(TermsGroup.TAXONOMY_TERMS).contains(term)))
669          .collect(Collectors.toList());
670}