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