+package org.simantics.history.rest;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.CacheControl;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import org.simantics.db.exception.DatabaseException;
+import org.simantics.history.HistoryException;
+import org.simantics.history.HistorySampler;
+import org.simantics.history.HistorySamplerItem;
+import org.simantics.history.HistorySamplerItem2;
+import org.simantics.history.csv.ExportInterpolation;
+import org.simantics.history.util.HistoryExportUtil;
+import org.simantics.history.util.StreamIterator;
+import org.simantics.history.util.ValueBand;
+
+import gnu.trove.list.array.TDoubleArrayList;
+
+@Path("history")
+@Produces(MediaType.APPLICATION_JSON)
+public class HistoryRestApi {
+
+ private static gnu.trove.map.TLongObjectMap<HistoryCache> historyCaches = new gnu.trove.map.hash.TLongObjectHashMap<>();
+
+ private static CacheControl cacheControl;
+
+ static {
+ cacheControl = new CacheControl();
+ cacheControl.setNoCache(true);
+ cacheControl.setNoStore(true);
+ }
+
+ @Path("/register")
+ @POST
+ public static synchronized Response register(@QueryParam("runId") long run) {
+ if (historyCaches.containsKey(run))
+ return Response.ok().build();
+ try {
+ HistoryCache cache = new HistoryCache(run);
+ historyCaches.put(run, cache);
+ return Response.ok().cacheControl(cacheControl).build();
+ } catch (DatabaseException e) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+ }
+
+ @Path("/unregister")
+ @POST
+ public static synchronized Response unregister(@QueryParam("runId") long run) {
+ HistoryCache cache = historyCaches.remove(run);
+ if (cache != null) {
+ cache.dispose();
+ }
+ return Response.ok().cacheControl(cacheControl).build();
+ }
+
+ @Path("/values")
+ @GET
+ public static Response activeHistoryValuesAndTimesWithTimeStep(@QueryParam("runId") long run,@QueryParam("itemId") long item,@QueryParam("startTime") double startTime,@QueryParam("endTime") double endTime,@QueryParam("timeWindow") double timeWindow, @QueryParam("timeStep")double timeStep) {
+ try {
+ HistoryCache cache = getOrCreateCache(run);
+ TDoubleArrayList doubles;
+ synchronized (cache) {
+ HistorySamplerItem hi = cache.getSamplerItem(item);
+ doubles= HistorySampler.sample(hi, startTime, endTime, timeWindow, timeStep, true);
+ }
+ return Response.ok(buildJSONResponse("values",doubles.toArray())).cacheControl(cacheControl).build();
+ } catch (HistoryException | IOException | DatabaseException e) {
+ return Response.serverError().build();
+ }
+
+ }
+ @Path("/lvalues")
+ @GET
+ public static Response sampleHistory(@QueryParam("runId")long run,@QueryParam("itemId") long item,@QueryParam("endTime") double endTime, @QueryParam("timeWindow") double timeWindow,@QueryParam("maxSamples") int maxSamples,@QueryParam("resample") boolean resample) {
+ try {
+ HistoryCache cache = getOrCreateCache(run);
+ TDoubleArrayList doubles = null;
+ synchronized (cache) {
+ HistorySamplerItem2 hi = cache.getSamplerItem2(item);
+ doubles = HistorySampler.sample(hi, endTime, timeWindow, maxSamples, resample);
+ }
+ return Response.ok(buildJSONResponse("values",doubles.toArray())).cacheControl(cacheControl).build();
+ } catch (HistoryException | IOException | DatabaseException e) {
+ return Response.serverError().build();
+ }
+ }
+
+ @Path("/samples")
+ @GET
+ public static Response sampleHistory(@QueryParam("runId")long run,@QueryParam("itemId") long item,@QueryParam("startTime") double startTime, @QueryParam("endTime") double endTime, @QueryParam("maxSamples") int maxSamples) {
+
+ if (maxSamples <= 0)
+ return Response.status(Status.BAD_REQUEST).build();
+ try {
+ HistoryCache cache = getOrCreateCache(run);
+ Map<String, Object> result = null;
+ synchronized (cache) {
+ result = _sampleHistory(cache, item, startTime, endTime, maxSamples);
+ }
+ return Response.ok(result).cacheControl(cacheControl).build();
+ } catch (HistoryException | DatabaseException e) {
+ return Response.serverError().build();
+ }
+ }
+
+ @Path("/multisamples")
+ @GET
+ public static Response sampleHistories(@QueryParam("runId")long run,@QueryParam("itemId") final List<Long> items,@QueryParam("startTime") double startTime, @QueryParam("endTime") double endTime, @QueryParam("maxSamples") int maxSamples) {
+
+ if (maxSamples <= 0)
+ return Response.status(Status.BAD_REQUEST).build();
+ try {
+ HistoryCache cache = getOrCreateCache(run);
+ Map<String, Object> combined = new HashMap<>();
+ synchronized (cache) {
+ for (long item : items) {
+ Map<String, Object> result = _sampleHistory(cache, item, startTime, endTime, maxSamples);
+ combined.put(Long.toString(item), result);
+ }
+ }
+ return Response.ok(combined).cacheControl(cacheControl).build();
+ } catch (HistoryException | DatabaseException e) {
+ return Response.serverError().build();
+ }
+ }
+
+ @Path("/ranges")
+ @GET
+ public static Response ranges(@QueryParam("runId")long run,@QueryParam("itemId") final List<Long> items) {
+
+ try {
+ HistoryCache cache = getOrCreateCache(run);
+ Map<String, Object> combined = new HashMap<>();
+ synchronized (cache) {
+ for (long item : items) {
+ HistorySamplerItem2 hi = null;
+ try {
+ hi= cache.getSamplerItem2(item);
+ hi.open();
+ double first = hi.iter.getFirstTime();
+ double last = hi.iter.getLastTime();
+ combined.put(Long.toString(item), buildJSONResponse("start",first,"end",last));
+
+ } finally {
+ hi.close();
+ }
+
+ }
+ }
+ return Response.ok(combined).cacheControl(cacheControl).build();
+
+ } catch (HistoryException | DatabaseException e) {
+ return Response.serverError().build();
+ }
+ }
+ private static synchronized HistoryCache getOrCreateCache(long run) throws DatabaseException {
+ HistoryCache cache = historyCaches.get(run);
+ if (cache == null || cache.isDisposed()) {
+ cache = new HistoryCache(run);
+ historyCaches.put(run, cache);
+ }
+ return cache;
+ }
+
+
+
+ private static Map<String,Object> _sampleHistory(HistoryCache cache, long item, double startTime, double endTime, int maxSamples) throws DatabaseException, HistoryException {
+ HistorySamplerItem2 hi = null;
+ try {
+ hi = cache.getSamplerItem2(item);
+
+ double timeWindow = endTime - startTime;
+ //double timeStep = 0.0;
+ double timeStep = timeWindow / maxSamples;
+ double secondsPerPixel = timeWindow / (double) maxSamples;
+ hi.flush();
+ if (hi.iter == null)
+ hi.open(secondsPerPixel);
+ StreamIterator iter = hi.iter;
+ if (iter.isEmpty()) {
+ buildJSONResponse("time",new double[0],"value",new double[0]);
+ }
+ double dataFrom = iter.getFirstTime();
+ double dataEnd = iter.getLastTime();
+ boolean hasAnyValues = dataFrom != Double.MAX_VALUE && dataEnd != -Double.MAX_VALUE;
+ if (!hasAnyValues) {
+ return buildJSONResponse("time",new double[0],"value",new double[0]);
+ }
+ double from = Math.max(startTime, dataFrom);
+ double end = Math.min(endTime, dataEnd);
+ iter.gotoTime(startTime);
+
+ double time = from;
+
+ BigDecimal bigTime = new BigDecimal(String.valueOf(time));
+ BigDecimal bigTimeStep = new BigDecimal(String.valueOf(timeStep));
+
+ TDoubleArrayList times = new TDoubleArrayList();
+ TDoubleArrayList values = new TDoubleArrayList();
+ TDoubleArrayList min = new TDoubleArrayList();
+ TDoubleArrayList max = new TDoubleArrayList();
+ TDoubleArrayList avg = new TDoubleArrayList();
+ if (!iter.gotoTime(time)) {
+ return buildJSONResponse("time",new double[0],"value",new double[0]);
+ }
+
+
+ ExportInterpolation interpolation = ExportInterpolation.LINEAR_INTERPOLATION;
+ do {
+ //System.out.println("process " + time + " " + iter.getValueBand() + " (ignore = " + ignore + ")");
+
+ // Check for valid value
+ if ( iter.hasValidValue() ) {
+
+ // Write time
+ times.add(time);
+ // Write value
+ Object value = iter.getValueBand().getValue();
+ //System.out.print("Add value : " + value);
+ if (value instanceof Number) {
+ if (value instanceof Float || value instanceof Double) {
+ switch (interpolation) {
+ case PREVIOUS_SAMPLE:
+ values.add(((Number) value).doubleValue());
+ break;
+
+ case LINEAR_INTERPOLATION:
+ if (time != iter.getValueBand().getTimeDouble() && iter.hasNext()) {
+ // Interpolate
+ int currentIndex = iter.getIndex();
+ ValueBand band = iter.getValueBand();
+ //double t1 = band.getTimeDouble();
+ Number v1 = (Number) value;
+ double t12 = band.getEndTimeDouble();
+ iter.next();
+ double t2 = iter.getValueBand().getTimeDouble();
+ Number v2 = (Number) iter.getValueBand().getValue();
+ iter.gotoIndex(currentIndex);
+
+ double vs = v1.doubleValue();
+ if (time > t12)
+ vs = HistoryExportUtil.biglerp(t12, v1.doubleValue(), t2, v2.doubleValue(), time);
+
+ values.add(vs);
+
+ } else {
+ // Exact timestamp match, or last sample.
+ // Don't interpolate nor extrapolate.
+ values.add(((Number) value).doubleValue());
+ }
+ break;
+
+ default:
+ throw new UnsupportedOperationException("Unsupported interpolation: " + interpolation);
+ }
+ if (iter.getValueBand().hasMax())
+ max.add(((Number) iter.getValueBand().getMax()).doubleValue());
+ else
+ max.add(Double.NaN);
+ if (iter.getValueBand().hasMin())
+ min.add(((Number) iter.getValueBand().getMin()).doubleValue());
+ else
+ min.add(Double.NaN);
+ if (iter.getValueBand().hasAvg())
+ avg.add(((Number) iter.getValueBand().getAvg()).doubleValue());
+ else
+ avg.add(Double.NaN);
+ } else {
+ throw new IllegalStateException("Value is not a number " + value);
+ }
+ } else if (value instanceof Boolean) {
+ values.add( (Boolean)value ? 1.0: 0.0);
+ } else {
+ throw new IllegalStateException("Value is not a number " + value);
+ }
+
+ }
+
+
+ // Read next values, and the following times
+ if ( timeStep>0.0 ) {
+ bigTime = bigTime.add(bigTimeStep);
+ time = bigTime.doubleValue();
+ } else {
+ // Get smallest end time that is larger than current time
+ Double nextTime = null;
+ //System.out.println(" time = "+time);
+ if(!iter.hasNext()) {
+ duplicateLastDataPoint(values);
+ times.add(end);
+ break;
+ }
+ Double itemNextTime = iter.getNextTime( time );
+ //System.out.println(" "+iter.toString()+" nextTime="+itemNextTime);
+ if ( nextTime == null || ( nextTime > itemNextTime && !itemNextTime.equals( time ) ) ) nextTime = itemNextTime;
+ if ( nextTime == null || nextTime.equals( time ) ) break;
+ time = nextTime;
+ }
+
+ boolean hasMore = false;
+
+ iter.proceedToTime(time);
+ if (HistoryExportUtil.contains(iter, time)) hasMore = true;
+
+ if (!hasMore) {
+ if (time <= end) {
+ duplicateLastDataPoint(values);
+ times.add(end);
+ }
+ break;
+ }
+
+ } while (time <= end);
+
+ return buildJSONResponse("time", times.toArray(),"value",values.toArray(), "min", min.toArray(), "max", max.toArray(),"avg", avg.toArray());
+ } finally {
+ if (hi != null) {
+ try {
+ hi.close();
+ } catch (Throwable err) {
+ }
+ }
+ }
+ }
+
+ private static void duplicateLastDataPoint(TDoubleArrayList data) {
+ double lastValue = data.get(data.size() - 1);
+ //System.out.println("Duplicating last sample value " + lastValue + " @ " + timestamp);
+ data.add(lastValue);
+ }
+
+ private static Map<String, Object> buildJSONResponse(Object... keyValues) {
+ if ((keyValues.length % 2) != 0)
+ throw new IllegalArgumentException("Invalid amount of arguments! " + Arrays.toString(keyValues));
+ Map<String, Object> results = new HashMap<>(keyValues.length / 2);
+ for (int i = 0; i < keyValues.length; i += 2) {
+ Object key = keyValues[i];
+ Object value = keyValues[i + 1];
+ if (!(key instanceof String))
+ throw new IllegalArgumentException("Key with index " + i + " is not String");
+ results.put((String) key, value);
+ }
+ return results;
+ }
+
+}