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  /**
145   * The recorded date specified as the eventDate string and the individual year, month, day and/or
146   * startDayOfYear, endDayOfYear are contradictory.
147   */
148  RECORDED_DATE_MISMATCH(WARNING, TermsGroup.RECORDED_DATE_TERMS),
149
150  /** A (partial) invalid date is given, such as a non-existent date, zero month, etc. */
151  RECORDED_DATE_INVALID(WARNING, TermsGroup.RECORDED_DATE_TERMS),
152
153  /**
154   * The recorded date is highly unlikely, falling either into the future or representing a very old
155   * date before 1600 thus predating modern taxonomy.
156   */
157  RECORDED_DATE_UNLIKELY(WARNING, TermsGroup.RECORDED_DATE_TERMS),
158
159  /** Matching to the taxonomic backbone can only be done using a fuzzy, non-exact match. */
160  TAXON_MATCH_FUZZY(WARNING, TermsGroup.TAXONOMY_TERMS),
161
162  /**
163   * Matching to the taxonomic backbone can only be done on a higher rank and not the scientific
164   * name.
165   */
166  TAXON_MATCH_HIGHERRANK(WARNING, TermsGroup.TAXONOMY_TERMS),
167
168  /**
169   * Matching to the taxonomic backbone can only be done on a species level, but the occurrence was
170   * in fact considered a broader species aggregate/complex.
171   *
172   * @see <a
173   *     href="https://github.com/gbif/portal-feedback/issues/2935">gbif/portal-feedback#2935</a>
174   */
175  TAXON_MATCH_AGGREGATE(WARNING, TermsGroup.TAXONOMY_TERMS),
176
177  /**
178   * The scientificNameID was not used when mapping the record to the GBIF backbone. This may
179   * indicate one of
180   *
181   * <ul>
182   *   <li>The ID uses a pattern not configured for use by GBIF
183   *   <li>The ID did not uniquely(!) identify a concept in the checklist
184   *   <li>The ID found a concept in the checklist which did not map to the backbone
185   *   <li>A different ID was used, or the record names were used as no ID lookup successfully
186   *       linked to the backbone
187   * </ul>
188   *
189   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
190   */
191  TAXON_MATCH_SCIENTIFIC_NAME_ID_IGNORED(INFO, DwcTerm.scientificNameID),
192
193  /**
194   * The taxonConceptID was not used when mapping the record to the GBIF backbone. This may indicate
195   * one of
196   *
197   * <ul>
198   *   <li>The ID uses a pattern not configured for use by GBIF
199   *   <li>The ID did not uniquely(!) identify a concept in the checklist
200   *   <li>The ID found a concept in the checklist which did not map to the backbone
201   *   <li>A different ID was used, or the record names were used as no ID lookup successfully
202   *       linked to the backbone
203   * </ul>
204   *
205   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
206   */
207  TAXON_MATCH_TAXON_CONCEPT_ID_IGNORED(INFO, DwcTerm.taxonConceptID),
208
209  /**
210   * The taxonID was not used when mapping the record to the GBIF backbone. This may indicate one of
211   *
212   * <ul>
213   *   <li>The ID uses a pattern not configured for use by GBIF
214   *   <li>The ID did not uniquely(!) identify a concept in the checklist
215   *   <li>The ID found a concept in the checklist which did not map to the backbone
216   *   <li>A different ID was used, or the record names were used as no ID lookup successfully
217   *       linked to the backbone
218   * </ul>
219   *
220   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
221   */
222  TAXON_MATCH_TAXON_ID_IGNORED(INFO, DwcTerm.taxonID),
223
224  /**
225   * The scientificNameID matched a known pattern, but it was not found in the associated checklist.
226   * The backbone lookup was performed using either the names or a different ID on the record. This
227   * may indicate a poorly formatted identifier or may be caused by a newly created ID that isn't
228   * yet known in the version of the published checklist.
229   *
230   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
231   */
232  SCIENTIFIC_NAME_ID_NOT_FOUND(WARNING, DwcTerm.scientificNameID),
233
234  /**
235   * The taxonConceptID matched a known pattern, but it was not found in the associated checklist.
236   * The backbone lookup was performed using either the names or a different ID on the record. This
237   * may indicate a poorly formatted identifier or may be caused by a newly created ID that isn't
238   * yet known in the version of the published checklist.
239   *
240   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
241   */
242  TAXON_CONCEPT_ID_NOT_FOUND(WARNING, DwcTerm.taxonConceptID),
243
244  /**
245   * The taxonID matched a known pattern, but it was not found in the associated checklist. The
246   * backbone lookup was performed using either the names or a different ID on the record. This may
247   * indicate a poorly formatted identifier or may be caused by a newly created ID that isn't yet
248   * known in the version of the published checklist.
249   *
250   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
251   */
252  TAXON_ID_NOT_FOUND(WARNING, DwcTerm.taxonID),
253
254  /**
255   * The scientificName provided in the occurrence record does not precisely match the name in the
256   * registered checklist when using the scientificNameID, taxonID or taxonConceptID to look it up.
257   * Publishers are advised to check the IDs are correct, or update the formatting of the names on
258   * their records.
259   *
260   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
261   */
262  SCIENTIFIC_NAME_AND_ID_INCONSISTENT(
263      WARNING,
264      DwcTerm.scientificNameID,
265      DwcTerm.taxonID,
266      DwcTerm.taxonConceptID,
267      DwcTerm.scientificName),
268
269  /**
270   * Matching to the taxonomic backbone cannot be done because there was no match at all, or several
271   * matches with too little information to keep them apart (potentially homonyms).
272   */
273  TAXON_MATCH_NONE(WARNING, TermsGroup.TAXONOMY_TERMS),
274
275  /**
276   * The GBIF Backbone concept was found using the scientificNameID, taxonID or taxonConceptID, but
277   * it differs from what would have been found if the classification names on the record were used.
278   * This may indicate a gap in the GBIF backbone, a poor mapping between the checklist and the
279   * backbone, or a mismatch between the classification names and the declared IDs (scientificNameID
280   * or taxonConceptID) on the occurrence record itself.
281   *
282   * @see <a href="https://github.com/gbif/pipelines/issues/217">gbif/pipelines#217</a>
283   */
284  TAXON_MATCH_NAME_AND_ID_AMBIGUOUS(WARNING, TermsGroup.TAXONOMY_TERMS),
285
286  /**
287   * Set if supplied depth is not given in the metric system, for example using feet instead of
288   * meters
289   */
290  DEPTH_NOT_METRIC(WARNING, DwcTerm.minimumDepthInMeters, DwcTerm.maximumDepthInMeters),
291
292  /** Set if depth is larger than 11,000m or negative. */
293  DEPTH_UNLIKELY(WARNING, DwcTerm.minimumDepthInMeters, DwcTerm.maximumDepthInMeters),
294
295  /** Set if supplied minimum depth > maximum depth */
296  DEPTH_MIN_MAX_SWAPPED(WARNING, DwcTerm.minimumDepthInMeters, DwcTerm.maximumDepthInMeters),
297
298  /** Set if depth is a non-numeric value */
299  DEPTH_NON_NUMERIC(WARNING, DwcTerm.minimumDepthInMeters, DwcTerm.maximumDepthInMeters),
300
301  /** Set if elevation is above the troposphere (17km) or below 11km (Mariana Trench). */
302  ELEVATION_UNLIKELY(WARNING, DwcTerm.minimumElevationInMeters, DwcTerm.maximumElevationInMeters),
303
304  /** Set if supplied minimum elevation > maximum elevation */
305  ELEVATION_MIN_MAX_SWAPPED(
306      WARNING, DwcTerm.minimumElevationInMeters, DwcTerm.maximumElevationInMeters),
307
308  /**
309   * Set if supplied elevation is not given in the metric system, for example using feet instead of
310   * meters
311   */
312  ELEVATION_NOT_METRIC(WARNING, DwcTerm.minimumElevationInMeters, DwcTerm.maximumElevationInMeters),
313
314  /** Set if elevation is a non-numeric value */
315  ELEVATION_NON_NUMERIC(
316      WARNING, DwcTerm.minimumElevationInMeters, DwcTerm.maximumElevationInMeters),
317
318  /**
319   * A (partial) invalid date is given for dc:modified, such as a nonexistent date, zero month, etc.
320   */
321  MODIFIED_DATE_INVALID(WARNING, DcTerm.modified),
322
323  /** The date given for dc:modified is in the future or predates Unix time (1970). */
324  MODIFIED_DATE_UNLIKELY(WARNING, DcTerm.modified),
325
326  /** The date given for dwc:dateIdentified is in the future or before Linnean times (1700). */
327  IDENTIFIED_DATE_UNLIKELY(WARNING, DwcTerm.dateIdentified),
328
329  /** The date given for dwc:dateIdentified is invalid and can't be interpreted at all. */
330  IDENTIFIED_DATE_INVALID(WARNING, DwcTerm.dateIdentified),
331
332  /**
333   * The given basis of record is impossible to interpret or significantly different from the
334   * recommended vocabulary.
335   */
336  BASIS_OF_RECORD_INVALID(WARNING, DwcTerm.basisOfRecord),
337
338  /**
339   * The given type status is impossible to interpret or significantly different from the
340   * recommended vocabulary.
341   */
342  TYPE_STATUS_INVALID(WARNING, DwcTerm.typeStatus),
343
344  /** The given type status contains some words that express uncertainty. */
345  SUSPECTED_TYPE(WARNING, DwcTerm.typeStatus),
346
347  /** An invalid date is given for dc:created of a multimedia object. */
348  MULTIMEDIA_DATE_INVALID(WARNING),
349
350  /** An invalid URI is given for a multimedia object. */
351  MULTIMEDIA_URI_INVALID(WARNING),
352
353  /** An invalid URI is given for dc:references. */
354  REFERENCES_URI_INVALID(WARNING, DcTerm.references),
355
356  /** An error occurred during interpretation, leaving the record interpretation incomplete. */
357  INTERPRETATION_ERROR(ERROR),
358
359  /** The individual count value is not a positive integer */
360  INDIVIDUAL_COUNT_INVALID(WARNING, DwcTerm.individualCount),
361
362  /** Example: individual count value > 0, but occurrence status is absent. */
363  INDIVIDUAL_COUNT_CONFLICTS_WITH_OCCURRENCE_STATUS(WARNING, DwcTerm.individualCount),
364
365  /** Occurrence status value can't be assigned to {@link OccurrenceStatus} */
366  OCCURRENCE_STATUS_UNPARSABLE(WARNING, DwcTerm.occurrenceStatus),
367
368  /** Occurrence status was inferred from the individual count value */
369  OCCURRENCE_STATUS_INFERRED_FROM_INDIVIDUAL_COUNT(WARNING, DwcTerm.occurrenceStatus),
370
371  /** Occurrence status was inferred from basis of records */
372  OCCURRENCE_STATUS_INFERRED_FROM_BASIS_OF_RECORD(WARNING, DwcTerm.occurrenceStatus),
373
374  /** The date given for dwc:georeferencedDate is in the future or before Linnean times (1700). */
375  GEOREFERENCED_DATE_UNLIKELY(WARNING, DwcTerm.georeferencedDate),
376
377  /** The date given for dwc:georeferencedDate is invalid and can't be interpreted at all. */
378  GEOREFERENCED_DATE_INVALID(WARNING, DwcTerm.georeferencedDate),
379
380  /** The given institution matches with more than 1 GRSciColl institution. */
381  AMBIGUOUS_INSTITUTION(INFO, TermsGroup.INSTITUTION_TERMS),
382
383  /** The given collection matches with more than 1 GRSciColl collection. */
384  AMBIGUOUS_COLLECTION(INFO, TermsGroup.COLLECTION_TERMS),
385
386  /** The given institution couldn't be matched with any GRSciColl institution. */
387  INSTITUTION_MATCH_NONE(INFO, TermsGroup.INSTITUTION_TERMS),
388
389  /** The given collection couldn't be matched with any GRSciColl collection. */
390  COLLECTION_MATCH_NONE(INFO, TermsGroup.COLLECTION_TERMS),
391
392  /**
393   * The given institution was fuzzily matched to a GRSciColl institution. This can happen when
394   * either the code or the ID don't match or when the institution name is used instead of the code.
395   */
396  INSTITUTION_MATCH_FUZZY(INFO, TermsGroup.INSTITUTION_TERMS),
397
398  /**
399   * The given collection was fuzzily matched to a GRSciColl collection. This can happen when either
400   * the code or the ID don't match or when the collection name is used instead of the code.
401   */
402  COLLECTION_MATCH_FUZZY(INFO, TermsGroup.COLLECTION_TERMS),
403
404  /** The collection matched doesn't belong to the institution matched. */
405  INSTITUTION_COLLECTION_MISMATCH(
406      INFO, ArrayUtils.addAll(TermsGroup.INSTITUTION_TERMS, TermsGroup.INSTITUTION_TERMS)),
407
408  /**
409   * The given owner institution is different than the given institution. Therefore we assume it
410   * could be on loan and we don't link it to the occurrence.
411   *
412   * <p>Deprecated by {@link #DIFFERENT_OWNER_INSTITUTION}.
413   */
414  @Deprecated
415  POSSIBLY_ON_LOAN(INFO, TermsGroup.INSTITUTION_TERMS),
416
417  /**
418   * The given owner institution is different than the given institution. Therefore we assume it
419   * doesn't belong to the institution and we don't link it to the occurrence.
420   */
421  DIFFERENT_OWNER_INSTITUTION(INFO, TermsGroup.INSTITUTION_TERMS),
422
423  /** Era or erathem was inferred from a parent rank. */
424  ERA_OR_ERATHEM_INFERRED_FROM_PARENT_RANK(
425      INFO, DwcTerm.earliestEraOrLowestErathem, DwcTerm.latestEraOrHighestErathem),
426  /** Period or system was inferred from a parent rank. */
427  PERIOD_OR_SYSTEM_INFERRED_FROM_PARENT_RANK(
428      INFO, DwcTerm.earliestPeriodOrLowestSystem, DwcTerm.latestPeriodOrHighestSystem),
429  /** Epoch or series was inferred from a parent rank. */
430  EPOCH_OR_SERIES_INFERRED_FROM_PARENT_RANK(
431      INFO, DwcTerm.earliestEpochOrLowestSeries, DwcTerm.latestEpochOrHighestSeries),
432  /** Age or stage was inferred from a parent rank. */
433  AGE_OR_STAGE_INFERRED_FROM_PARENT_RANK(
434      INFO, DwcTerm.earliestAgeOrLowestStage, DwcTerm.latestAgeOrHighestStage),
435
436  /** The eon or eonothem provided belongs to another rank. */
437  EON_OR_EONOTHEM_RANK_MISMATCH(
438      INFO, DwcTerm.earliestEonOrLowestEonothem, DwcTerm.latestEonOrHighestEonothem),
439  /** The era or erathem provided belongs to another rank. */
440  ERA_OR_ERATHEM_RANK_MISMATCH(
441      INFO, DwcTerm.earliestEraOrLowestErathem, DwcTerm.latestEraOrHighestErathem),
442  /** The period or system provided belongs to another rank. */
443  PERIOD_OR_SYSTEM_RANK_MISMATCH(
444      INFO, DwcTerm.earliestPeriodOrLowestSystem, DwcTerm.latestPeriodOrHighestSystem),
445  /** The period or series provided belongs to another rank. */
446  EPOCH_OR_SERIES_RANK_MISMATCH(
447      INFO, DwcTerm.earliestEpochOrLowestSeries, DwcTerm.latestEpochOrHighestSeries),
448  /** The age or stage provided belongs to another rank. */
449  AGE_OR_STAGE_RANK_MISMATCH(
450      INFO, DwcTerm.earliestAgeOrLowestStage, DwcTerm.latestAgeOrHighestStage),
451
452  /** The earliest eon or eonothem has to be earlier than the latest. */
453  EON_OR_EONOTHEM_INVALID_RANGE(
454      INFO, DwcTerm.earliestEonOrLowestEonothem, DwcTerm.latestEonOrHighestEonothem),
455  /** The era or erathem has to be earlier than the latest. */
456  ERA_OR_ERATHEM_INVALID_RANGE(
457      INFO, DwcTerm.earliestEraOrLowestErathem, DwcTerm.latestEraOrHighestErathem),
458  /** The period or system has to be earlier than the latest. */
459  PERIOD_OR_SYSTEM_INVALID_RANGE(
460      INFO, DwcTerm.earliestPeriodOrLowestSystem, DwcTerm.latestPeriodOrHighestSystem),
461  /** The period or series has to be earlier than the latest. */
462  EPOCH_OR_SERIES_INVALID_RANGE(
463      INFO, DwcTerm.earliestEpochOrLowestSeries, DwcTerm.latestEpochOrHighestSeries),
464  /** The age or stage has to be earlier than the latest. */
465  AGE_OR_STAGE_INVALID_RANGE(
466      INFO, DwcTerm.earliestAgeOrLowestStage, DwcTerm.latestAgeOrHighestStage),
467
468  /** The era or erathem don't belong to the eon or eonothem. */
469  EON_OR_EONOTHEM_AND_ERA_OR_ERATHEM_MISMATCH(
470      INFO,
471      DwcTerm.earliestEonOrLowestEonothem,
472      DwcTerm.latestEonOrHighestEonothem,
473      DwcTerm.earliestEraOrLowestErathem,
474      DwcTerm.latestEraOrHighestErathem),
475
476  /** The period or system don't belong to the era or erathem. */
477  ERA_OR_ERATHEM_AND_PERIOD_OR_SYSTEM_MISMATCH(
478      INFO,
479      DwcTerm.earliestEraOrLowestErathem,
480      DwcTerm.latestEraOrHighestErathem,
481      DwcTerm.earliestPeriodOrLowestSystem,
482      DwcTerm.latestPeriodOrHighestSystem),
483
484  /** The epoch or series don't belong to the period or system. */
485  PERIOD_OR_SYSTEM_AND_EPOCH_OR_SERIES_MISMATCH(
486      INFO,
487      DwcTerm.earliestPeriodOrLowestSystem,
488      DwcTerm.latestPeriodOrHighestSystem,
489      DwcTerm.earliestEpochOrLowestSeries,
490      DwcTerm.latestEpochOrHighestSeries),
491
492  /** The age or stage don't belong to the epoch or series. */
493  EPOCH_OR_SERIES_AND_AGE_OR_STAGE_MISMATCH(
494      INFO,
495      DwcTerm.earliestEpochOrLowestSeries,
496      DwcTerm.latestEpochOrHighestSeries,
497      DwcTerm.earliestAgeOrLowestStage,
498      DwcTerm.latestAgeOrHighestStage),
499
500  /** Set when natural language text is detected in {@code dna_sequence}. */
501  NUCLEOTIDE_SEQUENCE_NATURAL_LANGUAGE(INFO, GbifDnaTerm.dna_sequence),
502
503  /** Set when leading or trailing sequence characters are trimmed. */
504  NUCLEOTIDE_SEQUENCE_ENDS_TRIMMED(INFO, GbifDnaTerm.dna_sequence),
505
506  /** Set when gaps or whitespace are removed from the sequence. */
507  NUCLEOTIDE_SEQUENCE_GAPS_REMOVED(INFO, GbifDnaTerm.dna_sequence),
508
509  /** Set when the sequence is invalid after normalization and validation. */
510  NUCLEOTIDE_SEQUENCE_INVALID(INFO, GbifDnaTerm.dna_sequence),
511
512  /** Set when the fraction of N bases exceeds the configured threshold. */
513  NUCLEOTIDE_SEQUENCE_HIGH_N_FRACTION(INFO, GbifDnaTerm.dna_sequence),
514
515  /** Set when the fraction of non-ACGTN bases exceeds the configured threshold. */
516  NUCLEOTIDE_SEQUENCE_HIGH_AMBIGUITY(INFO, GbifDnaTerm.dna_sequence),
517
518  /** Set when {@code target_gene} does not resolve to a concept in the vocabulary. */
519  TARGET_GENE_INVALID(INFO, MixsTerm.target_gene),
520
521  /**
522   * Set when an infinite loop is detected in the event lineage.
523   */
524  PARENT_EVENT_INFINITE_LINEAGE(INFO, DwcTerm.parentEventID);
525
526  /**
527   * Simple helper nested class to allow grouping of Term mostly to increase readability of this
528   * class.
529   */
530  private static class TermsGroup {
531
532    static final Term[] COORDINATES_TERMS_NO_DATUM = {
533      DwcTerm.decimalLatitude,
534      DwcTerm.decimalLongitude,
535      DwcTerm.verbatimLatitude,
536      DwcTerm.verbatimLongitude,
537      DwcTerm.verbatimCoordinates
538    };
539
540    static final Term[] COORDINATES_TERMS = {
541      DwcTerm.decimalLatitude,
542      DwcTerm.decimalLongitude,
543      DwcTerm.verbatimLatitude,
544      DwcTerm.verbatimLongitude,
545      DwcTerm.verbatimCoordinates,
546      DwcTerm.geodeticDatum
547    };
548
549    static final Term[] COUNTRY_TERMS = {DwcTerm.country, DwcTerm.countryCode};
550
551    static final Term[] COORDINATES_COUNTRY_TERMS = {
552      DwcTerm.decimalLatitude,
553      DwcTerm.decimalLongitude,
554      DwcTerm.verbatimLatitude,
555      DwcTerm.verbatimLongitude,
556      DwcTerm.verbatimCoordinates,
557      DwcTerm.geodeticDatum,
558      DwcTerm.country,
559      DwcTerm.countryCode
560    };
561
562    static final Term[] RECORDED_DATE_TERMS = {
563      DwcTerm.eventDate,
564      DwcTerm.year,
565      DwcTerm.month,
566      DwcTerm.day,
567      DwcTerm.startDayOfYear,
568      DwcTerm.endDayOfYear
569    };
570
571    static final Term[] TAXONOMY_TERMS = {
572      DwcTerm.kingdom,
573      DwcTerm.phylum,
574      DwcTerm.class_,
575      DwcTerm.order,
576      DwcTerm.family,
577      DwcTerm.genus,
578      DwcTerm.scientificName,
579      DwcTerm.scientificNameAuthorship,
580      DwcTerm.genericName,
581      DwcTerm.specificEpithet,
582      DwcTerm.infraspecificEpithet,
583      DwcTerm.scientificNameID,
584      DwcTerm.taxonConceptID,
585      DwcTerm.taxonID,
586    };
587
588    static final Term[] INSTITUTION_TERMS = {
589      DwcTerm.institutionCode, DwcTerm.institutionID, DwcTerm.ownerInstitutionCode
590    };
591
592    static final Term[] COLLECTION_TERMS = {DwcTerm.collectionCode, DwcTerm.collectionID};
593  }
594
595  private final Set<Term> relatedTerms;
596  private final InterpretationRemarkSeverity severity;
597  private final boolean isDeprecated;
598
599  /** {@link OccurrenceIssue} not linked to any specific {@link Term}. */
600  OccurrenceIssue(InterpretationRemarkSeverity severity) {
601    this.severity = severity;
602    this.relatedTerms = Collections.emptySet();
603    this.isDeprecated = AnnotationUtils.isFieldDeprecated(OccurrenceIssue.class, this.name());
604  }
605
606  /** {@link OccurrenceIssue} linked to the provided {@link Term}. */
607  OccurrenceIssue(InterpretationRemarkSeverity severity, Term... relatedTerms) {
608    this.severity = severity;
609    this.relatedTerms = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(relatedTerms)));
610    this.isDeprecated = AnnotationUtils.isFieldDeprecated(OccurrenceIssue.class, this.name());
611  }
612
613  @Override
614  public String getId() {
615    return name();
616  }
617
618  @Override
619  public Set<Term> getRelatedTerms() {
620    return relatedTerms;
621  }
622
623  @Override
624  public InterpretationRemarkSeverity getSeverity() {
625    return severity;
626  }
627
628  @Override
629  public boolean isDeprecated() {
630    return isDeprecated;
631  }
632
633  /**
634   * All issues that indicate problems with the coordinates and thus should not be shown on maps.
635   */
636  public static final List<OccurrenceIssue> GEOSPATIAL_RULES =
637      Collections.unmodifiableList(
638          Arrays.asList(
639              ZERO_COORDINATE,
640              COORDINATE_OUT_OF_RANGE,
641              COORDINATE_INVALID,
642              COUNTRY_COORDINATE_MISMATCH,
643              PRESUMED_SWAPPED_COORDINATE,
644              PRESUMED_NEGATED_LONGITUDE,
645              PRESUMED_NEGATED_LATITUDE));
646
647  /** All issues related to taxonomic fields. */
648  public static final List<OccurrenceIssue> TAXONOMIC_RULES =
649      Set.of(OccurrenceIssue.values()).stream()
650          .filter(
651              issue ->
652                  issue.getRelatedTerms().stream()
653                      .anyMatch(term -> Set.of(TermsGroup.TAXONOMY_TERMS).contains(term)))
654          .collect(Collectors.toList());
655}