package com.github.windbender.core;
import java.util.Calendar;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.context.internal.ManagedSessionContext;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Interval;
import org.joda.time.LocalTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.github.windbender.dao.EventDAO;
import com.github.windbender.dao.HibernateUserDAO;
import com.github.windbender.dao.IdentificationDAO;
import com.github.windbender.dao.ImageRecordDAO;
import com.github.windbender.dao.SpeciesDAO;
import com.github.windbender.domain.Identification;
import com.github.windbender.domain.ImageEvent;
import com.github.windbender.domain.ImageRecord;
import com.github.windbender.domain.Project;
import com.github.windbender.domain.Species;
import com.github.windbender.domain.User;
import com.github.windbender.service.TimeZoneGetter;
import com.luckycatlabs.sunrisesunset.SunriseSunsetCalculator;
import com.luckycatlabs.sunrisesunset.dto.Location;
import com.yammer.dropwizard.lifecycle.Managed;
public class HibernateDataStore implements Managed, Runnable {
Logger log = LoggerFactory.getLogger(HibernateDataStore.class);
IdentificationDAO idDAO;
ImageRecordDAO irDAO;
SpeciesDAO speciesDAO;
HibernateUserDAO uDAO;
EventDAO eventDAO;
SessionFactory sessionFactory;
private TimeZoneGetter timeZoneGetter;
private final ConcurrentLinkedQueue<ImageRecord> eventSearchQueue;
public HibernateDataStore(IdentificationDAO idDAO, ImageRecordDAO irDAO, SpeciesDAO speciesDAO, HibernateUserDAO uDAO, EventDAO eventDAO, SessionFactory sessionFactory, TimeZoneGetter timeZoneGetter) {
this.idDAO = idDAO;
this.irDAO = irDAO;
this.speciesDAO = speciesDAO;
this.uDAO = uDAO;
this.eventDAO = eventDAO;
this.sessionFactory = sessionFactory;
eventSearchQueue = new ConcurrentLinkedQueue<ImageRecord>();
this.timeZoneGetter = timeZoneGetter;
}
public void addImage(ImageRecord newImage, Project currentProject) {
log.info("adding image "+newImage);
//TODO Do something here around current project and saving the image
// store the image in DS
String id =irDAO.create(newImage);
// find or make corresponding event
queueForEventSearch(newImage);
}
private int secondsDelta = 45;
private void queueForEventSearch(ImageRecord newImage) {
eventSearchQueue.add(newImage);
}
@Override
public void run() {
while(this.threadShouldRun) {
Session session = null;
try {
do {
session = sessionFactory.openSession();
ManagedSessionContext.bind(session);
ImageRecord ir = eventSearchQueue.poll();
if(ir == null) break;
ImageRecord addImage = irDAO.findById(ir.getId());
if(addImage == null) {
eventSearchQueue.add(ir);
break;
}
// search in DB for one that might work
DateTime before = ir.getDatetime().minusSeconds(secondsDelta);
DateTime after = ir.getDatetime().plusSeconds(secondsDelta);
List<ImageEvent> l =eventDAO.findEventsBetween(ir.getCameraID(),before, after);
if(l.size() >0) {
// ensure only one.
if(l.size() ==1) {
checkAndAddToFirst(addImage,ir, l);
} else {
// wow.. this could be good or bad. Let's find the closest and throw it in there.
Long distance = Long.MAX_VALUE;
ImageEvent choosenEvent = null;
for(ImageEvent ie: l) {
long delta = Math.abs(ir.getDatetime().getMillis() - ie.getEventStartTime().getMillis());
if(distance > delta) {
choosenEvent = ie;
distance = delta;
}
}
if(choosenEvent != null) {
addToEvent(ir,choosenEvent);
} else {
log.warn("No suitable event found for "+ir);
}
}
} else {
// make a new Event
ImageEvent ie = new ImageEvent();
ie.setCameraID(ir.getCameraID());
ie.setEventStartTime(ir.getDatetime());
if(addImage == null ) log.error("could not look up image, addImage is null is was "+ir.getId());
TypeOfDay tod = dayNightTwilight(ie, addImage);
ie.setTypeOfDay(tod);
if(addImage != null) {
ie.addImage(addImage);
irDAO.save(addImage);
} else {
// so probably we're too fast for this thing. let's push the orginal request and try it all again
eventSearchQueue.add(ir);
}
eventDAO.create(ie);
}
session.flush();
session.close();
ManagedSessionContext.unbind(sessionFactory);
session = null;
} while(true);
try {
Thread.sleep(250);
} catch (InterruptedException e) {
}
} catch(Exception e) {
log.error("Caught an exception processing events ",e);
} finally {
if(session != null) {
session.flush();
session.close();
session = null;
}
}
}
}
private TypeOfDay dayNightTwilight(ImageEvent ie, ImageRecord addImage) {
DateTime whenDT = ie.getEventStartTime();
double lat = addImage.getLat();
double lon = addImage.getLon();
return makeTimeOfDay(whenDT, lat, lon, timeZoneGetter,log);
}
public static TypeOfDay makeTimeOfDay(DateTime whenDT, double lat, double lon, TimeZoneGetter tzGetter, Logger lg) {
Location location = new Location(lat, lon);
Calendar when = whenDT.toCalendar(Locale.US);
DateTimeZone dtz = tzGetter.getTimeZone(new LatLonPair(lat,lon));
String tzid = dtz.getID();
LocalTime tod = new LocalTime(whenDT);
SunriseSunsetCalculator calculator = new SunriseSunsetCalculator(location, tzid);
Calendar rise = calculator.getNauticalSunriseCalendarForDate(when);
Calendar set = calculator.getNauticalSunsetCalendarForDate(when);
DateTime riseDatetime = (new DateTime(rise.getTime())).withZone(dtz);
LocalTime risedt = new LocalTime(riseDatetime);
DateTime setDatetime = (new DateTime(set.getTime())).withZone(dtz);
LocalTime setdt = new LocalTime(setDatetime);
if(tod.isBefore(risedt.minusMinutes(30))) return TypeOfDay.NIGHTTIME;
if(tod.isBefore(risedt.plusMinutes(90))) return TypeOfDay.MORNING;
if(tod.isBefore(setdt.minusMinutes(90))) return TypeOfDay.DAYTIME;
if(tod.isBefore(setdt.plusMinutes(30))) return TypeOfDay.EVENING;
return TypeOfDay.NIGHTTIME;
}
private void checkAndAddToFirst(ImageRecord ir, ImageRecord ir2, List<ImageEvent> l) {
ImageEvent ie = l.get(0);
addToEvent(ir, ie);
}
private void addToEvent(ImageRecord ir, ImageEvent ie) {
ImageRecord addImage = irDAO.findById(ir.getId());
ie.addImage(addImage);
irDAO.save(addImage);
eventDAO.save(ie);
}
public ImageRecord pollEventSearchQueue() {
return eventSearchQueue.poll();
}
class ImageEventHolder {
ImageEvent ie;
DateTime whenExpired;
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + getOuterType().hashCode();
result = prime * result + ((ie == null) ? 0 : ie.hashCode());
result = prime * result
+ ((whenExpired == null) ? 0 : whenExpired.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
ImageEventHolder other = (ImageEventHolder) obj;
if (!getOuterType().equals(other.getOuterType()))
return false;
if (ie == null) {
if (other.ie != null)
return false;
} else if (!ie.equals(other.ie))
return false;
if (whenExpired == null) {
if (other.whenExpired != null)
return false;
} else if (!whenExpired.equals(other.whenExpired))
return false;
return true;
}
private HibernateDataStore getOuterType() {
return HibernateDataStore.this;
}
}
public List<ImageRecord> getTimeOrderedImages() {
return irDAO.findAll();
}
public List<ImageEvent> getImageEvents() {
return eventDAO.findAll();
}
public NextEventRecord makeNextEventRecord(User u, Project currentProject, Long lastEventId) {
// We want to identify events which are
// a) haven't been previously identifed by this user
long start = System.currentTimeMillis();
List<Integer> done = this.eventDAO.findEventIdsIdentifiedByUser(currentProject.getId(),u);
long next = System.currentTimeMillis();
log.info("events done took "+(next-start));
SortedSet<Integer> doneSet = new TreeSet<Integer>(done);
List<Integer> ullist = this.eventDAO.findEventIdsUploadedByUser(currentProject.getId(),u);
SortedSet<Integer> yoursRemaining = new TreeSet<Integer>(ullist);
yoursRemaining.removeAll(done);
// b) have a number of zero or 1 previous identifications
int number =3;
List<Integer> lowNumber = this.eventDAO.findEventsIdsWithFewerThanIdentifications(currentProject.getId(),number);
long next2 = System.currentTimeMillis();
List<Integer> flagged = this.eventDAO.findFlaggedEventsIdsWithFewerThanIdentifications(currentProject.getId(), number+(number/2 + 1));
long next3 = System.currentTimeMillis();
log.info("so then low took "+(next2-next)+ " and flagged took "+(next3-next2));
SortedSet<Integer> lowNumberSet = new TreeSet<Integer>(lowNumber);
lowNumberSet.addAll(flagged);
lowNumberSet.removeAll(doneSet);
// ok, see where we're at
// all the events with less than number identifications.
// PLUS all the vents which have been flagged and have less than number plus number of times flagged. so one flag means one extra ID.
// no one can re-id an event.
// so once an event is flagged then it basically means new people need to come by and flag that event even more.
// Ok we could cache that set... or we could just pick one and go with it for now.
if((lowNumberSet.size() == 0 && yoursRemaining.size() == 0) ) {
NextEventRecord ner = new NextEventRecord(null);
ner.setNumberIdentified(doneSet.size());
ner.setRemainingToIdentify(0);
ner.setRemainingYoursToIdentify(0);
return ner;
}
SortedSet<Integer> useSet = null;
SortedSet<Integer> totalSet = new TreeSet<Integer>(lowNumberSet);
totalSet.addAll(lowNumberSet);
if(yoursRemaining.size() ==0) {
useSet = lowNumberSet;
} else {
useSet = yoursRemaining;
}
Long eventId = useSet.first().longValue();
// so there could be a race condition given the way event creation happens so we'll check both the current and NEXT events before we give up.
if(eventId.equals(lastEventId)) {
Iterator<Integer> nit = useSet.iterator();
// we know there is at least one so this first next() should always be fine
eventId = nit.next().longValue();
if(nit.hasNext()) {
eventId = nit.next().longValue();
}
if(eventId.equals(lastEventId)) {
// we just finished the last event, so return the "done" type record.
NextEventRecord ner = new NextEventRecord(null);
ner.setNumberIdentified(doneSet.size());
ner.setRemainingToIdentify(0);
return ner;
}
}
ImageEvent ie = eventDAO.findById(eventId);
// ImageEvent ie = null;
NextEventRecord ner = new NextEventRecord(ie);
ner.setNumberIdentified(doneSet.size());
ner.setRemainingToIdentify(totalSet.size());
ner.setRemainingYoursToIdentify(yoursRemaining.size());
return ner;
}
private SortedSet<Long> makeSetFromId(List<ImageEvent> done) {
SortedSet<Long> s = new TreeSet<Long>();
for(ImageEvent ie: done) {
s.add(ie.getId());
}
return s;
}
public long recordIdentification(IdentificationRequest idRequest, User u, Project currentProject) {
ImageRecord identifiedImage = null;
Species speciesIdentified = null;
ImageEvent identifiedEvent = null;
if(idRequest.getImageid() != null)
identifiedImage = irDAO.findById(idRequest.getImageid());
if(idRequest.getEventid() != null)
identifiedEvent = eventDAO.findById(idRequest.getEventid());
if(idRequest.getSpeciesId() == -1) {
// this means no species was seen.... what do we do here ? we should have a "none" species
speciesIdentified = speciesDAO.findByNameContains("none");
} else if(idRequest.getSpeciesId() == -2) {
speciesIdentified = speciesDAO.findByNameContains("unknown");
} else {
speciesIdentified = speciesDAO.findById(idRequest.getSpeciesId());
}
//TODO WE NEED SOMETHING HERE TO PREVENT CROSS PROJECT IDs from "storing"
Identification id = new Identification();
if(identifiedEvent != null) {
id.setIdentifiedEvent(identifiedEvent);
} else if(identifiedImage != null) {
id.setIdentifiedImage(identifiedImage);
} else {
throw new IllegalArgumentException("could not find either the event or the image supplied");
}
id.setIdentifier(u);
id.setTimeOfIdentification(new DateTime());
id.setSpeciesIdentified(speciesIdentified);
id.setNumberOfIndividuals(idRequest.getNumberOfAnimals());
long idid = idDAO.create(id);
return idid;
}
public void removeId(long idToClear, Project currentProject) {
//TODO confim the id is in the current project
idDAO.delete(idToClear);
}
public ImageRecord getRecordFromId(String id) {
ImageRecord identifiedImage = irDAO.findById(id);
return identifiedImage;
}
@Override
public void start() throws Exception {
threadShouldRun = true;
t = new Thread(this);
t.setName("Event Detector Thread");
t.start();
}
volatile boolean threadShouldRun = false;
Thread t;
@Override
public void stop() throws Exception {
threadShouldRun = false;
t.join();
}
}