diff --git a/src/main/java/no/nibio/vips/logic/controller/session/PointOfInterestBean.java b/src/main/java/no/nibio/vips/logic/controller/session/PointOfInterestBean.java index 756c09064b37127b4740f800b39b7c37dccae036..0ae305bfb937c69b7275b388dc53e8658d256255 100755 --- a/src/main/java/no/nibio/vips/logic/controller/session/PointOfInterestBean.java +++ b/src/main/java/no/nibio/vips/logic/controller/session/PointOfInterestBean.java @@ -483,7 +483,96 @@ public class PointOfInterestBean { .setParameter("name", dataSourceName) .getSingleResult(); } + + + /** + * Utility method to find the nearest weather station + * @param location + * @return + */ + public PointOfInterestWeatherStation findClosestWeatherStation(com.vividsolutions.jts.geom.Coordinate location) + { + return this.findClosestWeatherStation(location.x, location.y); + + } + + + + public PointOfInterestWeatherStation findClosestWeatherStation(Double longitude, Double latitude) + { + List<PointOfInterestWeatherStation> weatherStations = em.createNamedQuery("PointOfInterestWeatherStation.findAll", PointOfInterestWeatherStation.class) + .getResultList(); + return this.findClosestWeatherStation(longitude, latitude, weatherStations); + } + + /** + * Utility method to find the nearest weather station + * @param location + * @return + */ + public PointOfInterestWeatherStation findClosestWeatherStation(com.vividsolutions.jts.geom.Coordinate location, List<PointOfInterestWeatherStation> weatherStations) + { + return this.findClosestWeatherStation(location.x, location.y, weatherStations); + + } + /** + * Utility method to find the nearest weather station + * @param longitude + * @param latitude + * @return + */ + public PointOfInterestWeatherStation findClosestWeatherStation(Double longitude, Double latitude, List<PointOfInterestWeatherStation> weatherStations) + { + + + PointOfInterestWeatherStation retVal = null; + Double minimumDistance = null; // km + + for(PointOfInterestWeatherStation ws:weatherStations) + { + Double distance = this.calculateDistance(latitude, longitude, ws.getLatitude(), ws.getLongitude()); + if(minimumDistance == null || minimumDistance >= distance) + { + minimumDistance = distance; + retVal = ws; + } + } + return retVal; + } + + /** + * Based on <a href="http://www.movable-type.co.uk/scripts/latlong.html">this</a>. Explanation: + * <pre> + * This uses the "haversine" formula to calculate the great-circle distance between two points ? that is, the shortest distance over the earth?s surface ? giving an ?as-the-crow-flies? distance between the points (ignoring any hills, of course!). + Haversine formula: + a = sin²(∆lat/2) + cos(lat1).cos(lat2).sin²(∆long/2) + c = 2 * atan2(√a,√(1-a)) + d = R*c + where R is earth's radius (mean radius = 6,371km); + Note that angles need to be in radians to pass to trig functions! + * </pre> + * @param lat1 WGS84 latitude of point 1 + * @param long1 WGS84 longitude of point 1 + * @param lat2 WGS84 latitude of point 2 + * @param long2 WGS84 longitude of point 2 + * @return distance in km + */ + public Double calculateDistance(Double lat1, Double long1, Double lat2, Double long2) + { + Integer R = 6371; // km + Double dLat = Math.toRadians(lat2-lat1); + Double dLon = Math.toRadians(long2-long1); + lat1 = Math.toRadians(lat1); + lat2 = Math.toRadians(lat2); + + Double a = Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.sin(dLon/2) * Math.sin(dLon/2) * Math.cos(lat1) * Math.cos(lat2); + Double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + Double d = R * c; + + return d; + } } diff --git a/src/main/java/no/nibio/vips/logic/modules/wheatleafblotch/GrowthStageLocationDate.java b/src/main/java/no/nibio/vips/logic/modules/wheatleafblotch/GrowthStageLocationDate.java new file mode 100644 index 0000000000000000000000000000000000000000..7ffcbd167e81f03f81f6ee408861dfbc12928465 --- /dev/null +++ b/src/main/java/no/nibio/vips/logic/modules/wheatleafblotch/GrowthStageLocationDate.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2020 NIBIO <http://www.nibio.no/>. + * + * This file is part of VIPSLogic. + * VIPSLogic is free software: you can redistribute it and/or modify + * it under the terms of the NIBIO Open Source License as published by + * NIBIO, either version 1 of the License, or (at your option) any + * later version. + * + * VIPSLogic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * NIBIO Open Source License for more details. + * + * You should have received a copy of the NIBIO Open Source License + * along with VIPSLogic. If not, see <http://www.nibio.no/licenses/>. + * + */ + +package no.nibio.vips.logic.modules.wheatleafblotch; + +import java.io.Serializable; +import javax.persistence.Column; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * @copyright 2020 <a href="http://www.nibio.no/">NIBIO</a> + * @author Tor-Einar Skog <tor-einar.skog@nibio.no> + */ +@Entity +@Table(name = "growth_stage_location_date", schema = "wheatleafb") +@XmlRootElement +@NamedQueries({ + @NamedQuery(name = "GrowthStageLocationDate.findAll", query = "SELECT g FROM GrowthStageLocationDate g"), + @NamedQuery(name = "GrowthStageLocationDate.findByPointOfInterestId", query = "SELECT g FROM GrowthStageLocationDate g WHERE g.growthStageLocationDatePK.pointOfInterestId = :pointOfInterestId"), + @NamedQuery(name = "GrowthStageLocationDate.findByCropOrganismId", query = "SELECT g FROM GrowthStageLocationDate g WHERE g.growthStageLocationDatePK.cropOrganismId = :cropOrganismId"), + @NamedQuery(name = "GrowthStageLocationDate.findByCropOrganismIdAndPointOfInterestId", query = "SELECT g FROM GrowthStageLocationDate g WHERE g.growthStageLocationDatePK.cropOrganismId = :cropOrganismId AND g.growthStageLocationDatePK.pointOfInterestId = :pointOfInterestId") +}) +public class GrowthStageLocationDate implements Serializable, Comparable { + private static final long serialVersionUID = 1L; + @EmbeddedId + private GrowthStageLocationDatePK growthStageLocationDatePK; + @Column(name = "day_number") + private Integer dayNumber; + + + public GrowthStageLocationDate() { + } + + public GrowthStageLocationDate(GrowthStageLocationDatePK growthStateLocationPK) { + this.growthStageLocationDatePK = growthStateLocationPK; + } + + @Override + public int hashCode() { + int hash = 0; + hash += (getGrowthStageLocationDatePK() != null ? getGrowthStageLocationDatePK().hashCode() : 0); + return hash; + } + + @Override + public boolean equals(Object object) { + // TODO: Warning - this method won't work in the case the id fields are not set + if (!(object instanceof GrowthStageLocationDate)) { + return false; + } + GrowthStageLocationDate other = (GrowthStageLocationDate) object; + if ((this.getGrowthStageLocationDatePK() == null && other.getGrowthStageLocationDatePK() != null) || (this.getGrowthStageLocationDatePK() != null && !this.growthStageLocationDatePK.equals(other.growthStageLocationDatePK))) { + return false; + } + return true; + } + + @Override + public String toString() { + return "no.nibio.vips.logic.modules.wheatleafblotch.GrowthStageLocationDate[ growthStageLocationDatePK=" + getGrowthStageLocationDatePK() + " ]"; + } + + /** + * @return the dayNumber + */ + public Integer getDayNumber() { + return dayNumber; + } + + /** + * @param dayNumber the dayNumber to set + */ + public void setDayNumber(Integer dayNumber) { + this.dayNumber = dayNumber; + } + + @Override + public int compareTo(Object t) { + return this.getGrowthStageLocationDatePK().getGrowthStage().compareTo(((GrowthStageLocationDate) t).getGrowthStageLocationDatePK().getGrowthStage() + ); + } + + /** + * @return the growthStageLocationDatePK + */ + public GrowthStageLocationDatePK getGrowthStageLocationDatePK() { + return growthStageLocationDatePK; + } + + /** + * @param growthStageLocationDatePK the growthStageLocationDatePK to set + */ + public void setGrowthStageLocationDatePK(GrowthStageLocationDatePK growthStageLocationDatePK) { + this.growthStageLocationDatePK = growthStageLocationDatePK; + } + +} diff --git a/src/main/java/no/nibio/vips/logic/modules/wheatleafblotch/GrowthStageLocationDatePK.java b/src/main/java/no/nibio/vips/logic/modules/wheatleafblotch/GrowthStageLocationDatePK.java new file mode 100644 index 0000000000000000000000000000000000000000..d5c698723a76c73579c206754d2a0c7357884d51 --- /dev/null +++ b/src/main/java/no/nibio/vips/logic/modules/wheatleafblotch/GrowthStageLocationDatePK.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2020 NIBIO <http://www.nibio.no/>. + * + * This file is part of VIPSLogic. + * VIPSLogic is free software: you can redistribute it and/or modify + * it under the terms of the NIBIO Open Source License as published by + * NIBIO, either version 1 of the License, or (at your option) any + * later version. + * + * VIPSLogic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * NIBIO Open Source License for more details. + * + * You should have received a copy of the NIBIO Open Source License + * along with VIPSLogic. If not, see <http://www.nibio.no/licenses/>. + * + */ + +package no.nibio.vips.logic.modules.wheatleafblotch; + +import java.io.Serializable; +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.validation.constraints.NotNull; +import no.nibio.vips.logic.entity.Organism; +import no.nibio.vips.logic.entity.PointOfInterest; + +/** + * @copyright 2020 <a href="http://www.nibio.no/">NIBIO</a> + * @author Tor-Einar Skog <tor-einar.skog@nibio.no> + */ +@Embeddable +public class GrowthStageLocationDatePK implements Serializable { + + @Basic(optional = false) + @NotNull + @JoinColumn(name = "crop_organism_id", referencedColumnName = "organism_id") + @ManyToOne + private Organism cropOrganismId; + @Basic(optional = false) + @NotNull + @JoinColumn(name = "point_of_interest_id", referencedColumnName = "point_of_interest_id") + @ManyToOne + private PointOfInterest pointOfInterestId; + @Column(name = "growth_stage") + private Integer growthStage; + + + public GrowthStageLocationDatePK() { + } + + public GrowthStageLocationDatePK(Organism organism, PointOfInterest pointOfInterestId) { + this.cropOrganismId = organism; + this.pointOfInterestId = pointOfInterestId; + } + + public Organism getCropOrganismId() { + return cropOrganismId; + } + + public void setCropOrganismId(Organism cropOrganismId) { + this.cropOrganismId = cropOrganismId; + } + + public PointOfInterest getPointOfInterestId() { + return pointOfInterestId; + } + + public void setPointOfInterestId(PointOfInterest pointOfInterestId) { + this.pointOfInterestId = pointOfInterestId; + } + + public Integer getGrowthStage() { + return growthStage; + } + + public void setGrowthStage(Integer growthStage) { + this.growthStage = growthStage; + } + + @Override + public int hashCode() { + int hash = 0; + hash += (int) cropOrganismId.getOrganismId(); + hash += (int) pointOfInterestId.getPointOfInterestId(); + hash += (int) growthStage; + return hash; + } + + @Override + public boolean equals(Object object) { + // TODO: Warning - this method won't work in the case the id fields are not set + if (!(object instanceof GrowthStageLocationDatePK)) { + return false; + } + GrowthStageLocationDatePK other = (GrowthStageLocationDatePK) object; + if (this.cropOrganismId != other.cropOrganismId) { + return false; + } + if (this.pointOfInterestId != other.pointOfInterestId) { + return false; + } + if(! this.growthStage.equals(other.growthStage)){ + return false; + } + return true; + } + + @Override + public String toString() { + return "no.nibio.vips.logic.modules.wheatleafblotch.YieldLossPK[ organism=" + cropOrganismId + ", pointOfInterestId=" + pointOfInterestId + " ]"; + } + +} diff --git a/src/main/java/no/nibio/vips/logic/service/GrowthStageService.java b/src/main/java/no/nibio/vips/logic/service/GrowthStageService.java index 7803810bdb328bf803b0ce5db1ce9fb2c2ac410f..f87bca2424f82f44c37f42bb17604694da165e72 100644 --- a/src/main/java/no/nibio/vips/logic/service/GrowthStageService.java +++ b/src/main/java/no/nibio/vips/logic/service/GrowthStageService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 NIBIO <http://www.nibio.no/>. + * Copyright (c) 2020 NIBIO <http://www.nibio.no/>. * * This file is part of VIPSLogic. * VIPSLogic is free software: you can redistribute it and/or modify @@ -19,20 +19,19 @@ package no.nibio.vips.logic.service; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import java.text.SimpleDateFormat; +import com.vividsolutions.jts.geom.Coordinate; import java.time.LocalDate; import java.time.Month; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; -import java.util.Date; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.TimeZone; import java.util.stream.Collectors; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.GET; import javax.ws.rs.Path; @@ -41,16 +40,21 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; -import org.jboss.resteasy.annotations.GZIP; +import no.nibio.vips.logic.entity.Organism; +import no.nibio.vips.logic.entity.PointOfInterestWeatherStation; +import no.nibio.vips.logic.modules.wheatleafblotch.GrowthStageLocationDate; +import no.nibio.vips.logic.util.SessionControllerGetter; /** - * @copyright 2019 <a href="http://www.nibio.no/">NIBIO</a> + * @copyright 2020 <a href="http://www.nibio.no/">NIBIO</a> * @author Tor-Einar Skog <tor-einar.skog@nibio.no> */ @Path("rest/gs") public class GrowthStageService { @Context private HttpServletRequest httpServletRequest; + @PersistenceContext(unitName="VIPSLogic-PU") + EntityManager em; @GET @@ -63,30 +67,18 @@ public class GrowthStageService { @QueryParam("season") Integer season ) { + List<GrowthStageLocationDate> growthStageDatesForLocationAndCrop = this.getGrowthStageDatesForLocationAndCrop(location, organismId); //SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); //format.setTimeZone(TimeZone.getTimeZone("Europe/Oslo")); // TODO get from param or default is UTC List<Map<Integer,LocalDate>> retVal = new ArrayList<>(); List<Integer> growthStages = Arrays.stream(growthStagesStr.split(",")).map(str->Integer.valueOf(str)).collect(Collectors.toList()); - for(Integer growthStage: growthStages) - { + growthStages.stream().map((growthStage) -> { Map<Integer, LocalDate> gsDate = new HashMap<>(); - - if(growthStage.equals(32)) - { - gsDate.put(growthStage, LocalDate.of(season != null ? season : LocalDate.now().getYear(), Month.JUNE,10)); - } - else if(growthStage.equals(71)) - { - gsDate.put(growthStage, LocalDate.of(season != null ? season : LocalDate.now().getYear(), Month.JULY,25)); - } - else - { - gsDate.put(growthStage, LocalDate.of(season != null ? season : LocalDate.now().getYear(), Month.JANUARY,1)); - } + gsDate.put(growthStage, this.getDateForGrowthStage(growthStageDatesForLocationAndCrop, growthStage, season)); + return gsDate; + }).forEachOrdered((gsDate) -> { retVal.add(gsDate); - } - - + }); return Response.ok().entity(retVal).build(); } @@ -99,17 +91,117 @@ public class GrowthStageService { @QueryParam("location") String location ) { - Integer retVal; + + DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE; LocalDate date = LocalDate.parse(dateStr, formatter); - if(date.isBefore(LocalDate.of(LocalDate.now().getYear(), Month.JULY,1))) + List<GrowthStageLocationDate> growthStageDatesForLocationAndCrop = this.getGrowthStageDatesForLocationAndCrop(location, organismId); + //Integer[] retVal = {this.getGrowthStageForDate(growthStageDatesForLocationAndCrop, date)}; + List<Map<LocalDate,Integer>> retVal = new ArrayList<>(); + Map<LocalDate,Integer> actualRetVal = new HashMap<>(); + actualRetVal.put(date, this.getGrowthStageForDate(growthStageDatesForLocationAndCrop, date)); + retVal.add(actualRetVal); + return Response.ok().entity(retVal).build(); + } + + private Coordinate getLocationCoordinate(String location) + { + String[] lonLat = location.split(","); + Double longitude = Double.valueOf(lonLat[0]); + Double latitude = Double.valueOf(lonLat[1]); + Coordinate retVal = new Coordinate(longitude, latitude); + return retVal; + } + + private Integer getGrowthStageForDate(List<GrowthStageLocationDate> datesForGrowthStages, LocalDate date) + { + Integer dateDayNum = date.getDayOfYear(); + Collections.sort(datesForGrowthStages); + for(int i=0;i<datesForGrowthStages.size();i++) { - retVal = 32; + Integer gsDayNum1 = datesForGrowthStages.get(i).getDayNumber(); + // Case 1: Date is before the earliest registered GSDate in DB + // Case 2: Date is after the latest registered GSDate in DB + if((i==0 && dateDayNum < gsDayNum1 ) || (i == datesForGrowthStages.size()-1 && dateDayNum > gsDayNum1)) + { + return null; + } + // Case 3: date equals the current list item GSDate: Return as is + if(dateDayNum.equals(gsDayNum1)) + { + return datesForGrowthStages.get(i).getGrowthStageLocationDatePK().getGrowthStage(); + } + + Integer gsDayNum2 = datesForGrowthStages.get(i+1).getDayNumber(); + // Case 4 date is between current and next GSDate + if(dateDayNum > gsDayNum1 && dateDayNum < gsDayNum2) + { + // We do linear interpolation to find the gs + Float deltaGS = (float) (datesForGrowthStages.get(i+1).getGrowthStageLocationDatePK().getGrowthStage() - datesForGrowthStages.get(i).getGrowthStageLocationDatePK().getGrowthStage()) + / (gsDayNum2 - gsDayNum1); + // How many days per gs between the two items? + //Float deltaDay = (float) (datesForGrowthStages.get(i+1).getDayNumber() - datesForGrowthStages.get(i).getDayNumber()) / (gs2-gs1); + Integer addedGS = Math.round((dateDayNum - gsDayNum1) * deltaGS); + return datesForGrowthStages.get(i).getGrowthStageLocationDatePK().getGrowthStage() + addedGS; + } + // How many gs per day between the two items? + //Double deltaGS = (double) (gs2-gs1) / (datesForGrowthStages.get(i+1).getDayNumber() - datesForGrowthStages.get(i).getDayNumber()); } - else + return null; + } + + private LocalDate getDateForGrowthStage(List<GrowthStageLocationDate> datesForGrowthStages, Integer growthStage, Integer season) + { + Collections.sort(datesForGrowthStages); + for(int i=0;i<datesForGrowthStages.size();i++) { - retVal = 71; + Integer gs1 = datesForGrowthStages.get(i).getGrowthStageLocationDatePK().getGrowthStage(); + // Case 1: GS is lower than the lowest registered stage in DB + // Case 2: GS is higher than the highest registered stage in DB + if((i==0 && growthStage < gs1) || (i == datesForGrowthStages.size()-1 && growthStage > gs1)) + { + return null; + } + // Case 3: GS equals the current list item GS: Return as is + if(growthStage.equals(gs1)) + { + return LocalDate.ofYearDay(season, datesForGrowthStages.get(i).getDayNumber()); + } + + Integer gs2 = datesForGrowthStages.get(i+1).getGrowthStageLocationDatePK().getGrowthStage(); + // Case 4 GS is between current and next GS + if(growthStage > gs1 && growthStage < gs2) + { + // We do linear interpolation to find the date + + // How many days per gs between the two items? + Float deltaDay = (float) (datesForGrowthStages.get(i+1).getDayNumber() - datesForGrowthStages.get(i).getDayNumber()) / (gs2-gs1); + Integer addedDays = Math.round((growthStage - gs1) * deltaDay); + return LocalDate.ofYearDay(season, datesForGrowthStages.get(i).getDayNumber() + addedDays); + } + // How many gs per day between the two items? + //Double deltaGS = (double) (gs2-gs1) / (datesForGrowthStages.get(i+1).getDayNumber() - datesForGrowthStages.get(i).getDayNumber()); } - return Response.ok().entity(retVal).build(); + return null; + } + + private List<GrowthStageLocationDate> getGrowthStageDatesForLocationAndCrop(String location, Integer organismId) + { + List<PointOfInterestWeatherStation> stationsWithGrowthStageInfoForCrop = em.createNativeQuery( + "SELECT poi.*, poiws.* FROM public.point_of_interest_weather_station poiws, public.point_of_interest poi " + + "WHERE poi.point_of_interest_id IN(" + + " SELECT point_of_interest_id FROM wheatleafb.growth_stage_location_date " + + " WHERE crop_organism_id = :cropOrganismId " + + ")" + + "AND poiws.point_of_interest_id = poi.point_of_interest_id", + PointOfInterestWeatherStation.class) + .setParameter("cropOrganismId", organismId) + .getResultList(); + + PointOfInterestWeatherStation closestStation = SessionControllerGetter.getPointOfInterestBean().findClosestWeatherStation(this.getLocationCoordinate(location), stationsWithGrowthStageInfoForCrop); + return em.createNamedQuery("GrowthStageLocationDate.findByCropOrganismIdAndPointOfInterestId") + .setParameter("cropOrganismId", em.find(Organism.class, organismId)) + .setParameter("pointOfInterestId", closestStation) + .getResultList(); } }