package uk.ac.ox.zoo.seeg.abraid.mp.common.domain; import org.hibernate.annotations.*; import org.hibernate.annotations.CascadeType; import org.joda.time.DateTime; import javax.persistence.*; import javax.persistence.Entity; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.Table; /** * Represents an occurrence of a disease group, in a location, as reported by an alert. * * Copyright (c) 2014 University of Oxford */ @NamedQueries({ @NamedQuery( name = "getDiseaseOccurrencesByIds", query = DiseaseOccurrence.DISEASE_OCCURRENCE_BASE_QUERY + "where d.id in :diseaseOccurrenceIds" ), @NamedQuery( name = "getDiseaseOccurrencesByDiseaseGroupId", query = DiseaseOccurrence.DISEASE_OCCURRENCE_BASE_QUERY + "where d.diseaseGroup.id=:diseaseGroupId" ), @NamedQuery( name = "getDiseaseOccurrencesByDiseaseGroupIdAndStatuses", query = DiseaseOccurrence.DISEASE_OCCURRENCE_BASE_QUERY + "inner join fetch d.location.country " + "where d.diseaseGroup.id=:diseaseGroupId and d.status in :statuses" ), @NamedQuery( name = "getDiseaseOccurrencesForExistenceCheck", query = "from DiseaseOccurrence where diseaseGroup=:diseaseGroup and location=:location " + "and alert=:alert and occurrenceDate=:occurrenceDate and biasDisease is null" // Using biasDisease instead of status BIAS, as bias points can be BIAS or FAILED_QC, neither // should be considered for duplicates as they may be purged ), @NamedQuery( name = "getDiseaseOccurrencesYetToBeReviewedByExpert", query = DiseaseOccurrence.DISEASE_OCCURRENCE_BASE_QUERY + "where d.diseaseGroup.validatorDiseaseGroup.id=:validatorDiseaseGroupId " + "and (:userIsSeeg = true or d.diseaseGroup.lastModelRunPrepDate is not null) " + "and d.status = 'IN_REVIEW' " + "and d.id not in " + "(select diseaseOccurrence.id from DiseaseOccurrenceReview where expert.id=:expertId)" ), @NamedQuery( name = "getDiseaseOccurrencesInValidation", query = DiseaseOccurrence.DISEASE_OCCURRENCE_BASE_QUERY + "where d.diseaseGroup.id=:diseaseGroupId and d.status = 'IN_REVIEW'" ), @NamedQuery( name = "getDiseaseOccurrencesYetToHaveFinalWeightingAssigned", query = DiseaseOccurrence.DISEASE_OCCURRENCE_BASE_QUERY + "where d.diseaseGroup.id=:diseaseGroupId " + "and d.status in :statuses " + "and d.finalWeighting is null " ), @NamedQuery( name = "getDistinctLocationsCountForTriggeringModelRunWithoutLastModelRunClause", query = DiseaseOccurrence.NEW_LOCATION_COUNT_QUERY ), @NamedQuery( name = "getDistinctLocationsCountForTriggeringModelRunWithLastModelRunClause", query = DiseaseOccurrence.NEW_LOCATION_COUNT_QUERY + DiseaseOccurrence.NEW_LOCATION_COUNT_QUERY_NOT_IN_LAST_RUN_CLAUSE ), @NamedQuery( name = "getDiseaseOccurrenceStatistics", query = "select new uk.ac.ox.zoo.seeg.abraid.mp.common.domain.DiseaseOccurrenceStatistics" + "(count(*), coalesce(sum(case location.isModelEligible when true then 1 else 0 end), 0)," + " min(occurrenceDate), max(occurrenceDate)) " + "from DiseaseOccurrence " + "where diseaseGroup.id=:diseaseGroupId " + "and status in ('READY', 'IN_REVIEW', 'AWAITING_BATCHING')" ), @NamedQuery( name = "getDiseaseOccurrencesForBatching", query = DiseaseOccurrence.DISEASE_OCCURRENCE_BASE_QUERY + "where d.diseaseGroup.id=:diseaseGroupId " + "and d.status = 'AWAITING_BATCHING' " + "and d.occurrenceDate between :batchStartDate and :batchEndDate " + "and " + DiseaseOccurrence.NOT_GOLD_STANDARD ), @NamedQuery( name = "getDiseaseOccurrencesForBatchingInitialisation", query = DiseaseOccurrence.DISEASE_OCCURRENCE_BASE_QUERY + "where d.diseaseGroup.id=:diseaseGroupId " + "and d.status = 'READY' " + "and " + DiseaseOccurrence.NOT_GOLD_STANDARD ), @NamedQuery( name = "getNumberOfDiseaseOccurrencesEligibleForModelRun", query = "select count(*) " + "from DiseaseOccurrence " + "where diseaseGroup.id=:diseaseGroupId " + "and status in ('READY', 'IN_REVIEW', 'AWAITING_BATCHING') " + "and location.isModelEligible is TRUE " + "and occurrenceDate between :startDate and :endDate" ), @NamedQuery( name = "getCountOfUnfilteredBespokeBiasOccurrences", query = "select count(*) " + "from DiseaseOccurrence d " + "where d.biasDisease.id=:diseaseGroupId" ), @NamedQuery( name = "getEstimateCountOfFilteredBespokeBiasOccurrences", // This only an estimate as we can not apply the occurrence date filter used in // 'getBespokeBiasOccurrences' outside of the context of a model run as the date range is unknown query = "select count(*) " + "from DiseaseOccurrence d " + "where d.biasDisease.id=:diseaseGroupId " + "and d.status = 'BIAS' " + DiseaseOccurrence.BIAS_OCCURRENCE_LOCATION_FILTER_CLAUSES ), @NamedQuery( name = "getEstimateCountOfFilteredDefaultBiasOccurrences", // This only an estimate as we can not apply the occurrence date filter used in // 'getDefaultBiasOccurrencesForModelRun' outside of a model run as the date range is unknown query = "select count(*) " + "from DiseaseOccurrence d " + "where d.diseaseGroup.id<>:diseaseGroupId " + "and d.status NOT IN ('DISCARDED_FAILED_QC', 'BIAS') " + DiseaseOccurrence.BIAS_OCCURRENCE_AGENT_TYPE_FILTER_CLAUSE + DiseaseOccurrence.BIAS_OCCURRENCE_LOCATION_FILTER_CLAUSES ), @NamedQuery( name = "getBespokeBiasOccurrencesForModelRun", query = DiseaseOccurrence.DISEASE_OCCURRENCE_BASE_QUERY + "where d.biasDisease.id=:diseaseGroupId " + "and d.status = 'BIAS' " + DiseaseOccurrence.BIAS_OCCURRENCE_LOCATION_FILTER_CLAUSES + DiseaseOccurrence.OCCURRENCE_DATE_FILTER_CLAUSE ), @NamedQuery( name = "getDefaultBiasOccurrencesForModelRun", query = DiseaseOccurrence.DISEASE_OCCURRENCE_BASE_QUERY + "where d.diseaseGroup.id<>:diseaseGroupId " + "and d.status NOT IN ('DISCARDED_FAILED_QC', 'BIAS') " + // As this data set is for sample bias, we don't need to apply our // normal filters (READY/final weighting), but we don't want bias points if there aren't // bias points uploaded for this disease DiseaseOccurrence.BIAS_OCCURRENCE_AGENT_TYPE_FILTER_CLAUSE + DiseaseOccurrence.BIAS_OCCURRENCE_LOCATION_FILTER_CLAUSES + DiseaseOccurrence.OCCURRENCE_DATE_FILTER_CLAUSE ), @NamedQuery( name = "getDiseaseOccurrencesForTrainingPredictor", query = DiseaseOccurrence.DISEASE_OCCURRENCE_BASE_QUERY + "where d.diseaseGroup.id=:diseaseGroupId " + "and d.status = 'READY' " + "and d.location.isModelEligible is TRUE " + "and d.distanceFromDiseaseExtent is not null " + "and d.environmentalSuitability is not null " + "and d.expertWeighting is not null " + "and d.occurrenceDate > :cutOffDate" ), @NamedQuery( name = "deleteDiseaseOccurrencesByBiasDiseaseId", query = "delete from DiseaseOccurrence where biasDisease.id=:diseaseGroupId" ) }) @Entity @Table(name = "disease_occurrence") public class DiseaseOccurrence { /** * An HQL fragment used as a basis for disease occurrence queries. It ensures that Hibernate populate the objects * and their parents using one select statement. */ public static final String DISEASE_OCCURRENCE_BASE_QUERY = "from DiseaseOccurrence as d " + "inner join fetch d.location " + "inner join fetch d.alert " + "inner join fetch d.alert.feed " + "inner join fetch d.alert.feed.provenance " + "inner join fetch d.diseaseGroup "; /** * An HQL fragment used to exclude gold standard occurrences from queries. */ public static final String NOT_GOLD_STANDARD = "d.alert.feed.provenance.name <> '" + ProvenanceNames.MANUAL_GOLD_STANDARD + "'"; /** * The final weighting assigned to a "gold standard" disease occurrence. */ public static final double GOLD_STANDARD_FINAL_WEIGHTING = 1.0; /** * An HQL fragment used to count the new location for model triggering. */ public static final String NEW_LOCATION_COUNT_QUERY = "select count(distinct d.location.id) " + "from DiseaseOccurrence as d " + "where d.diseaseGroup.id=:diseaseGroupId " + // Model eligible locations "and d.location.isModelEligible is TRUE " + // Linked to occurrences that were not available for use in the last model run "and d.status='READY' " + "and (" + "(d.expertWeighting is NULL and createdDate > :cutoffForAutomaticallyValidated) " + "or " + "(d.expertWeighting is not NULL and createdDate > :cutoffForManuallyValidated) " + ") " + // Which are in areas of interest (new places) "and (" + "environmentalSuitability <= :maxEnvironmentalSuitability " + "or " + "distanceFromDiseaseExtent >= :minDistanceFromDiseaseExtent" + ")"; /** * An HQL fragment used to count the new location for model triggering. * "That were not used in the last model run" */ public static final String NEW_LOCATION_COUNT_QUERY_NOT_IN_LAST_RUN_CLAUSE = " and d.location.id not in :locationsFromLastModelRun"; /** A HQL fragment to choose between global and tropical gaul codes. */ public static final String PICK_EXTENT_GAUL_CODE = "CASE :isGlobal WHEN true " + " THEN d.location.adminUnitGlobalGaulCode " + " ELSE d.location.adminUnitTropicalGaulCode " + "END"; /** A HQL fragment to filter occurrences by location when selecting a bias data set. * Filters by model eligibiliy (size) and disease extent (within only) */ public static final String BIAS_OCCURRENCE_LOCATION_FILTER_CLAUSES = "and d.location.isModelEligible is TRUE " + "and (" + DiseaseOccurrence.PICK_EXTENT_GAUL_CODE + ") " + "in (" + AdminUnitDiseaseExtentClass.EXTENT_GAUL_CODES_BY_DISEASE_GROUP_ID + ") "; /** A HQL fragment to filter occurrences by disease group agent type when selecting a bias data set. */ public static final String BIAS_OCCURRENCE_AGENT_TYPE_FILTER_CLAUSE = "and (:shouldFilterBiasDataByAgentType is FALSE or d.diseaseGroup.agentType=:agentType) "; /** A HQL fragment to filter occurrences within an occurrence date range. */ public static final String OCCURRENCE_DATE_FILTER_CLAUSE = "and d.occurrenceDate between :startDate and :endDate "; // The primary key. @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; // The disease group that occurred. @ManyToOne @Cascade(CascadeType.SAVE_UPDATE) @JoinColumn(name = "disease_group_id", nullable = false) private DiseaseGroup diseaseGroup; // The location of this occurrence. @ManyToOne @Cascade(CascadeType.SAVE_UPDATE) @JoinColumn(name = "location_id", nullable = false) private Location location; // The alert containing this occurrence. @ManyToOne @Cascade(CascadeType.SAVE_UPDATE) @JoinColumn(name = "alert_id", nullable = false) private Alert alert; // The database row creation date. @Column(name = "created_date", insertable = false, updatable = false) @Generated(value = GenerationTime.INSERT) @Type(type = "org.jadira.usertype.dateandtime.joda.PersistentDateTime") private DateTime createdDate; // The status of this occurrence. @Column @Enumerated(EnumType.STRING) private DiseaseOccurrenceStatus status; // The suitability of the environment for this disease group to exist in this location. @Column(name = "env_suitability") private Double environmentalSuitability; // The distance between this location and the edge of the disease extent. The value is positive if the location // is outside of the extent, or negative if inside. @Column(name = "distance_from_extent") private Double distanceFromDiseaseExtent; // The weighting as calculated from experts' responses during data validation process. @Column(name = "expert_weighting") private Double expertWeighting; // The weighting as predicted via the system (which may include machine learning). @Column(name = "machine_weighting") private Double machineWeighting; // The validation weighting used in the data weighting formula. // Takes the value of the expertWeighting if it exists, otherwise the machineWeighting value. @Column(name = "validation_weighting") private Double validationWeighting; // The final weighting to be used in the model run, // combining location resolution weighting, feed weighting, disease group type weighting and validation weighting. @Column(name = "final_weighting") private Double finalWeighting; // The final weighting to be used in later model runs, following a refactor where location's spatial resolution // weighting is handled separately. This value combines feed weighting, disease group type weighting and validation // weighting. @Column(name = "final_weighting_excl_spatial") private Double finalWeightingExcludingSpatial; // The date of the disease occurrence. @Column(name = "occurrence_date") @Type(type = "org.jadira.usertype.dateandtime.joda.PersistentDateTime") private DateTime occurrenceDate; // The disease group for which this occurrence is part of a bias set (or null). @ManyToOne @JoinColumn(name = "bias_disease_group_id", nullable = true) private DiseaseGroup biasDisease; public DiseaseOccurrence() { } public DiseaseOccurrence(int id) { this.id = id; } public DiseaseOccurrence(DiseaseGroup diseaseGroup, DateTime occurrenceDate, Location location, Alert alert) { this.diseaseGroup = diseaseGroup; this.occurrenceDate = occurrenceDate; this.location = location; this.alert = alert; } public DiseaseOccurrence(Integer id, DiseaseGroup diseaseGroup, Location location, Alert alert, DiseaseOccurrenceStatus status, Double finalWeighting, DateTime occurrenceDate) { this.id = id; this.diseaseGroup = diseaseGroup; this.location = location; this.alert = alert; this.status = status; this.finalWeighting = finalWeighting; this.occurrenceDate = occurrenceDate; } public DiseaseOccurrence(DateTime createdDate) { this.createdDate = createdDate; } public Integer getId() { return id; } public DiseaseGroup getDiseaseGroup() { return diseaseGroup; } public ValidatorDiseaseGroup getValidatorDiseaseGroup() { return diseaseGroup.getValidatorDiseaseGroup(); } public void setDiseaseGroup(DiseaseGroup diseaseGroup) { this.diseaseGroup = diseaseGroup; } public Location getLocation() { return location; } public void setLocation(Location location) { this.location = location; } public Alert getAlert() { return alert; } public void setAlert(Alert alert) { this.alert = alert; } public DateTime getCreatedDate() { return createdDate; } public DiseaseOccurrenceStatus getStatus() { return status; } public void setStatus(DiseaseOccurrenceStatus status) { this.status = status; } public Double getEnvironmentalSuitability() { return environmentalSuitability; } public void setEnvironmentalSuitability(Double environmentalSuitability) { this.environmentalSuitability = environmentalSuitability; } public Double getDistanceFromDiseaseExtent() { return distanceFromDiseaseExtent; } public void setDistanceFromDiseaseExtent(Double distanceFromDiseaseExtent) { this.distanceFromDiseaseExtent = distanceFromDiseaseExtent; } public Double getExpertWeighting() { return expertWeighting; } public void setExpertWeighting(Double expertWeighting) { this.expertWeighting = expertWeighting; } public Double getMachineWeighting() { return machineWeighting; } public void setMachineWeighting(Double machineWeighting) { this.machineWeighting = machineWeighting; } public Double getValidationWeighting() { return validationWeighting; } public void setValidationWeighting(Double validationWeighting) { this.validationWeighting = validationWeighting; } public Double getFinalWeighting() { return finalWeighting; } public void setFinalWeighting(Double finalWeighting) { this.finalWeighting = finalWeighting; } public Double getFinalWeightingExcludingSpatial() { return finalWeightingExcludingSpatial; } public void setFinalWeightingExcludingSpatial(Double finalWeightingExcludingSpatial) { this.finalWeightingExcludingSpatial = finalWeightingExcludingSpatial; } public DateTime getOccurrenceDate() { return occurrenceDate; } public void setOccurrenceDate(DateTime occurrenceDate) { this.occurrenceDate = occurrenceDate; } public DiseaseGroup getBiasDisease() { return biasDisease; } public void setBiasDisease(DiseaseGroup biasDisease) { this.biasDisease = biasDisease; } ///COVERAGE:OFF - generated code ///CHECKSTYLE:OFF AvoidInlineConditionalsCheck|LineLengthCheck|MagicNumberCheck|NeedBracesCheck - generated code @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; DiseaseOccurrence that = (DiseaseOccurrence) o; if (alert != null ? !alert.equals(that.alert) : that.alert != null) return false; if (createdDate != null ? !createdDate.equals(that.createdDate) : that.createdDate != null) return false; if (diseaseGroup != null ? !diseaseGroup.equals(that.diseaseGroup) : that.diseaseGroup != null) return false; if (distanceFromDiseaseExtent != null ? !distanceFromDiseaseExtent.equals(that.distanceFromDiseaseExtent) : that.distanceFromDiseaseExtent != null) return false; if (environmentalSuitability != null ? !environmentalSuitability.equals(that.environmentalSuitability) : that.environmentalSuitability != null) return false; if (expertWeighting != null ? !expertWeighting.equals(that.expertWeighting) : that.expertWeighting != null) return false; if (finalWeighting != null ? !finalWeighting.equals(that.finalWeighting) : that.finalWeighting != null) return false; if (finalWeightingExcludingSpatial != null ? !finalWeightingExcludingSpatial.equals(that.finalWeightingExcludingSpatial) : that.finalWeightingExcludingSpatial != null) return false; if (id != null ? !id.equals(that.id) : that.id != null) return false; if (location != null ? !location.equals(that.location) : that.location != null) return false; if (machineWeighting != null ? !machineWeighting.equals(that.machineWeighting) : that.machineWeighting != null) return false; if (occurrenceDate != null ? !occurrenceDate.equals(that.occurrenceDate) : that.occurrenceDate != null) return false; if (status != null ? !status.equals(that.status) : that.status != null) return false; if (validationWeighting != null ? !validationWeighting.equals(that.validationWeighting) : that.validationWeighting != null) return false; if (biasDisease != null ? !biasDisease.equals(that.biasDisease) : that.biasDisease != null) return false; return true; } @Override public int hashCode() { int result = id != null ? id.hashCode() : 0; result = 31 * result + (diseaseGroup != null ? diseaseGroup.hashCode() : 0); result = 31 * result + (location != null ? location.hashCode() : 0); result = 31 * result + (alert != null ? alert.hashCode() : 0); result = 31 * result + (createdDate != null ? createdDate.hashCode() : 0); result = 31 * result + (status != null ? status.hashCode() : 0); result = 31 * result + (environmentalSuitability != null ? environmentalSuitability.hashCode() : 0); result = 31 * result + (distanceFromDiseaseExtent != null ? distanceFromDiseaseExtent.hashCode() : 0); result = 31 * result + (expertWeighting != null ? expertWeighting.hashCode() : 0); result = 31 * result + (machineWeighting != null ? machineWeighting.hashCode() : 0); result = 31 * result + (validationWeighting != null ? validationWeighting.hashCode() : 0); result = 31 * result + (finalWeighting != null ? finalWeighting.hashCode() : 0); result = 31 * result + (finalWeightingExcludingSpatial != null ? finalWeightingExcludingSpatial.hashCode() : 0); result = 31 * result + (occurrenceDate != null ? occurrenceDate.hashCode() : 0); result = 31 * result + (biasDisease != null ? biasDisease.hashCode() : 0); return result; } ///CHECKSTYLE:ON ///COVERAGE:ON }