diff --git a/pdb-plotting/src/main/java/org/lucares/pdb/plot/api/PlotSettings.java b/pdb-plotting/src/main/java/org/lucares/pdb/plot/api/PlotSettings.java index e8c6ce0..d2caa00 100644 --- a/pdb-plotting/src/main/java/org/lucares/pdb/plot/api/PlotSettings.java +++ b/pdb-plotting/src/main/java/org/lucares/pdb/plot/api/PlotSettings.java @@ -20,6 +20,10 @@ public class PlotSettings { private int width; + private int thumbnailMaxWidth = 0; + + private int thumbnailMaxHeight = 0; + private List groupBy; private Limit limitBy; @@ -62,6 +66,22 @@ public class PlotSettings { this.width = width; } + public int getThumbnailMaxWidth() { + return thumbnailMaxWidth; + } + + public void setThumbnailMaxWidth(final int thumbnailMaxWidth) { + this.thumbnailMaxWidth = thumbnailMaxWidth; + } + + public int getThumbnailMaxHeight() { + return thumbnailMaxHeight; + } + + public void setThumbnailMaxHeight(final int thumbnailMaxHeight) { + this.thumbnailMaxHeight = thumbnailMaxHeight; + } + public List getGroupBy() { return groupBy; } @@ -160,20 +180,22 @@ public class PlotSettings { @Override public String toString() { - return "PlotSettings [query=" + query + ", height=" + height + ", width=" + width + ", groupBy=" + groupBy + return "PlotSettings [query=" + query + ", height=" + height + ", width=" + width + ", thumbnailMaxWidth=" + + thumbnailMaxWidth + ", thumbnailMaxHeight=" + thumbnailMaxHeight + ", groupBy=" + groupBy + ", limitBy=" + limitBy + ", limit=" + limit + ", dateFrom=" + dateFrom + ", dateRange=" + dateRange - + ", axisScale=" + yAxisScale + ", aggregate="+aggregate+", keyOutside="+keyOutside+"]"; + + ", yAxisScale=" + yAxisScale + ", aggregate=" + aggregate + ", keyOutside=" + keyOutside + + ", plotType=" + plotType + "]"; } - public void setAggregate(AggregateHandler aggregate) { + public void setAggregate(final AggregateHandler aggregate) { this.aggregate = aggregate; } - + public AggregateHandler getAggregate() { return aggregate; } - - public void setKeyOutside(boolean keyOutside) { + + public void setKeyOutside(final boolean keyOutside) { this.keyOutside = keyOutside; } @@ -181,10 +203,10 @@ public class PlotSettings { return keyOutside; } - public void setPlotType(PlotType plotType) { + public void setPlotType(final PlotType plotType) { this.plotType = plotType; } - + public PlotType getPlotType() { return plotType; } diff --git a/pdb-plotting/src/main/java/org/lucares/recommind/logs/PercentilePlot.java b/pdb-plotting/src/main/java/org/lucares/recommind/logs/PercentilePlot.java index 4529efe..cb2e0f0 100644 --- a/pdb-plotting/src/main/java/org/lucares/recommind/logs/PercentilePlot.java +++ b/pdb-plotting/src/main/java/org/lucares/recommind/logs/PercentilePlot.java @@ -101,7 +101,7 @@ public class PercentilePlot implements ConcretePlotter { gnuplotSettings.setKeyOutside(plotSettings.isKeyOutside()); gnuplot.plot(gnuplotSettings, dataSeries); - return new PlotResult(outputFile, dataSeries); + return new PlotResult(outputFile, dataSeries, null); // TODO thumbail } catch (final InterruptedException e) { Thread.currentThread().interrupt(); throw new IllegalStateException("Plotting was interrupted."); diff --git a/pdb-plotting/src/main/java/org/lucares/recommind/logs/PlotResult.java b/pdb-plotting/src/main/java/org/lucares/recommind/logs/PlotResult.java index 9d59c70..caed6dc 100644 --- a/pdb-plotting/src/main/java/org/lucares/recommind/logs/PlotResult.java +++ b/pdb-plotting/src/main/java/org/lucares/recommind/logs/PlotResult.java @@ -6,11 +6,13 @@ import java.util.List; public class PlotResult { private final Path imagePath; private final List dataSeries; + private final Path thumbnail; - public PlotResult(final Path imagePath, final List dataSeries) { + public PlotResult(final Path imagePath, final List dataSeries, final Path thumbnail) { super(); this.imagePath = imagePath; this.dataSeries = dataSeries; + this.thumbnail = thumbnail; } public Path getImageName() { @@ -21,6 +23,14 @@ public class PlotResult { return imagePath; } + public Path getThumbnailName() { + return thumbnail.getFileName(); + } + + public Path getThumbnailPath() { + return thumbnail; + } + public List getDataSeries() { return dataSeries; } diff --git a/pdb-plotting/src/main/java/org/lucares/recommind/logs/ScatterPlot.java b/pdb-plotting/src/main/java/org/lucares/recommind/logs/ScatterPlot.java index 4f62530..e7d5a22 100644 --- a/pdb-plotting/src/main/java/org/lucares/recommind/logs/ScatterPlot.java +++ b/pdb-plotting/src/main/java/org/lucares/recommind/logs/ScatterPlot.java @@ -1,5 +1,7 @@ package org.lucares.recommind.logs; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; @@ -21,6 +23,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; +import javax.imageio.ImageIO; + import org.lucares.pdb.api.Entry; import org.lucares.pdb.api.GroupResult; import org.lucares.pdb.api.Result; @@ -118,7 +122,10 @@ public class ScatterPlot implements ConcretePlotter { gnuplotSettings.setKeyOutside(plotSettings.isKeyOutside()); gnuplot.plot(gnuplotSettings, dataSeries); - return new PlotResult(outputFile, dataSeries); + final Path thumbnail = createOptionalThumbnail(outputFile, plotSettings.getThumbnailMaxWidth(), + plotSettings.getThumbnailMaxHeight()); + + return new PlotResult(outputFile, dataSeries, thumbnail); } catch (final InterruptedException e) { Thread.currentThread().interrupt(); throw new IllegalStateException("Plotting was interrupted."); @@ -130,6 +137,44 @@ public class ScatterPlot implements ConcretePlotter { } } + private static BufferedImage resizeImage(final BufferedImage originalImage, final Integer img_width, + final Integer img_height) { + final int type = originalImage.getType() == 0 ? BufferedImage.TYPE_INT_ARGB : originalImage.getType(); + final BufferedImage resizedImage = new BufferedImage(img_width, img_height, type); + final Graphics2D g = resizedImage.createGraphics(); + g.drawImage(originalImage, 0, 0, img_width, img_height, null); + g.dispose(); + + return resizedImage; + } + + private Path createOptionalThumbnail(final Path originalImage, final int thumbnailMaxWidth, + final int thumbnailMaxHeight) { + + Path result; + if (thumbnailMaxWidth > 0 && thumbnailMaxHeight > 0) { + try { + final long start = System.nanoTime(); + final BufferedImage image = ImageIO.read(originalImage.toFile()); + + final BufferedImage thumbnail = resizeImage(image, thumbnailMaxWidth, thumbnailMaxHeight); + final Path thumbnailPath = originalImage.getParent() + .resolve(originalImage.getFileName() + ".thumbnail.jpg"); + + ImageIO.write(thumbnail, "JPG", thumbnailPath.toFile()); + LOGGER.info("thumbnail creation: " + (System.nanoTime() - start) / 1_000_000.0 + "ms"); + result = thumbnailPath; + } catch (final IOException e) { + LOGGER.warn("failed to scale image", e); + result = null; + } + } else { + result = null; + } + + return result; + } + private void defineXAxis(final GnuplotSettings gnuplotSettings, final OffsetDateTime minDate, final OffsetDateTime maxDate) { diff --git a/pdb-ui/src/main/java/org/lucares/pdbui/PdbController.java b/pdb-ui/src/main/java/org/lucares/pdbui/PdbController.java index 4d93a10..851a1fc 100644 --- a/pdb-ui/src/main/java/org/lucares/pdbui/PdbController.java +++ b/pdb-ui/src/main/java/org/lucares/pdbui/PdbController.java @@ -101,7 +101,11 @@ public class PdbController implements HardcodedValues { final String imageUrl = WEB_IMAGE_OUTPUT_PATH + "/" + result.getImageName(); LOGGER.trace("image url: {}", imageUrl); - return new PlotResponse(DataSeries.toMap(result.getDataSeries()), imageUrl); + final String thumbnailUrl = result.getThumbnailPath() != null + ? WEB_IMAGE_OUTPUT_PATH + "/" + result.getThumbnailName() + : imageUrl; + + return new PlotResponse(DataSeries.toMap(result.getDataSeries()), imageUrl, thumbnailUrl); } catch (final NoDataPointsException e) { throw new NotFoundException(e); } finally { @@ -127,8 +131,8 @@ public class PdbController implements HardcodedValues { @RequestParam(name = "plotType", defaultValue = "SCATTER") final PlotType plotType, @RequestParam(name = "aggregate", defaultValue = "NONE") final Aggregate aggregate, @RequestParam(name = "keyOutside", defaultValue = "false") final boolean keyOutside, - @RequestParam(name = "height", defaultValue = "1080") final int height, - @RequestParam(name = "width", defaultValue = "1920") final int hidth) { + @RequestParam(name = "width", defaultValue = "1920") final int hidth, + @RequestParam(name = "height", defaultValue = "1080") final int height) { return (final OutputStream outputStream) -> { if (StringUtils.isBlank(query)) { diff --git a/pdb-ui/src/main/java/org/lucares/pdbui/PlotSettingsTransformer.java b/pdb-ui/src/main/java/org/lucares/pdbui/PlotSettingsTransformer.java index d44ae21..fac9235 100644 --- a/pdb-ui/src/main/java/org/lucares/pdbui/PlotSettingsTransformer.java +++ b/pdb-ui/src/main/java/org/lucares/pdbui/PlotSettingsTransformer.java @@ -24,6 +24,8 @@ class PlotSettingsTransformer { result.setPlotType(request.getPlotType()); result.setAggregate(toAggregateInternal(request.getAggregate())); result.setKeyOutside(request.isKeyOutside()); + result.setThumbnailMaxWidth(request.getThumbnailMaxWidth()); + result.setThumbnailMaxHeight(request.getThumbnailMaxHeight()); return result; } diff --git a/pdb-ui/src/main/java/org/lucares/pdbui/domain/PlotRequest.java b/pdb-ui/src/main/java/org/lucares/pdbui/domain/PlotRequest.java index e0b3ce1..96f4e29 100644 --- a/pdb-ui/src/main/java/org/lucares/pdbui/domain/PlotRequest.java +++ b/pdb-ui/src/main/java/org/lucares/pdbui/domain/PlotRequest.java @@ -13,6 +13,10 @@ public class PlotRequest { private int width = 1000; + private int thumbnailMaxWidth = 0; + + private int thumbnailMaxHeight = 0; + private List groupBy; private Limit limitBy = Limit.NO_LIMIT; @@ -24,9 +28,9 @@ public class PlotRequest { private String dateFrom; private String dateRange; - + private PlotType plotType = PlotType.SCATTER; - + private Aggregate aggregate = Aggregate.NONE; private boolean keyOutside; @@ -55,6 +59,22 @@ public class PlotRequest { this.height = height; } + public int getThumbnailMaxWidth() { + return thumbnailMaxWidth; + } + + public void setThumbnailMaxWidth(final int thumbnailMaxWidth) { + this.thumbnailMaxWidth = thumbnailMaxWidth; + } + + public int getThumbnailMaxHeight() { + return thumbnailMaxHeight; + } + + public void setThumbnailMaxHeight(final int thumbnailMaxHeight) { + this.thumbnailMaxHeight = thumbnailMaxHeight; + } + @Override public String toString() { return query + ":" + height + "x" + width; @@ -110,27 +130,27 @@ public class PlotRequest { public void setAxisScale(final AxisScale yAxis) { this.yAxis = yAxis; } - + public PlotType getPlotType() { return plotType; } - public void setPlotType(PlotType plotType) { + public void setPlotType(final PlotType plotType) { this.plotType = plotType; } - public void setAggregate(Aggregate aggregate) { + public void setAggregate(final Aggregate aggregate) { this.aggregate = aggregate; } - + public Aggregate getAggregate() { return aggregate; } - public void setKeyOutside(boolean keyOutside) { + public void setKeyOutside(final boolean keyOutside) { this.keyOutside = keyOutside; } - + public boolean isKeyOutside() { return keyOutside; } diff --git a/pdb-ui/src/main/java/org/lucares/pdbui/domain/PlotResponse.java b/pdb-ui/src/main/java/org/lucares/pdbui/domain/PlotResponse.java index 3206bd8..3690ca6 100644 --- a/pdb-ui/src/main/java/org/lucares/pdbui/domain/PlotResponse.java +++ b/pdb-ui/src/main/java/org/lucares/pdbui/domain/PlotResponse.java @@ -5,10 +5,12 @@ import java.util.Map; public class PlotResponse { private String imageUrl = ""; private Map dataSeries; + private final String thumbnailUrl; - public PlotResponse(final Map dataSeries, final String imageUrl) { + public PlotResponse(final Map dataSeries, final String imageUrl, final String thumbnailUrl) { this.dataSeries = dataSeries; this.imageUrl = imageUrl; + this.thumbnailUrl = thumbnailUrl; } public String getImageUrl() { @@ -19,6 +21,10 @@ public class PlotResponse { this.imageUrl = imageUrl; } + public String getThumbnailUrl() { + return thumbnailUrl; + } + public Map getDataSeries() { return dataSeries; } @@ -29,6 +35,6 @@ public class PlotResponse { @Override public String toString() { - return imageUrl + " " + dataSeries; + return imageUrl + " " + dataSeries + " " + thumbnailUrl; } } diff --git a/pdb-ui/src/main/resources/resources/css/design.css b/pdb-ui/src/main/resources/resources/css/design.css index 8ac1cf5..af396ca 100644 --- a/pdb-ui/src/main/resources/resources/css/design.css +++ b/pdb-ui/src/main/resources/resources/css/design.css @@ -83,10 +83,13 @@ textarea { background: #eee; margin: 0; padding: 0; - overflow: hidden; + overflow: auto; position:relative; } +#result-view { +} + #filter-bar { grid-area: filter_bar; } @@ -139,11 +142,12 @@ textarea { margin-right:3px; } - - #result-image { height: 100%; } +#result-image img { + display:block; /* removes 3 pixels extra height around the image */ +} #prev_image, #next_image { position: absolute; @@ -175,6 +179,25 @@ textarea { top: 0; } +.dashboard-item { + width: 300px; + height: 200px; + margin: 10px; + float: left; + background: white; + box-shadow: 5px 5px 10px 0px #aaa; +} + +.dashboard-item img { + max-width: 300px; + max-height: 180px; + display:block; /* removes 3 pixels extra height around the image */ +} + +.dashboard-item fieldValue{ + +} + .center { display: flex; diff --git a/pdb-ui/src/main/resources/resources/js/ui.js b/pdb-ui/src/main/resources/resources/js/ui.js index 5bcf314..c086da1 100644 --- a/pdb-ui/src/main/resources/resources/js/ui.js +++ b/pdb-ui/src/main/resources/resources/js/ui.js @@ -207,14 +207,40 @@ Vue.component('result-view', { } }, template: ` -
-
-
+
+
+
{{ resultView.errorMessage }}
` }); +Vue.component('result-view-dashboard', { + props: ['dashboard'], + methods: { + }, + template: ` +
+ +
+ ` +}); + +Vue.component('result-view-dashboard-item', { + props: ['dashboardItem'], + template: ` +
+
{{ dashboardItem.error }}
+ +
{{ dashboardItem.fieldValue }}
+
` +}); + Vue.component('navigation-bar', { props: [], methods: { @@ -376,17 +402,22 @@ Vue.component('navigation-bar', { return date; } }, + computed: { + navigationDisabled: function() { + return !data.resultView.imageUrl; + } + }, template: ` ` }); @@ -418,8 +449,8 @@ Vue.component('search-bar', { plotCurrent(); } }, - dashboard: function (event) { - alert('dashboard'); + createNewDashboard: function (event) { + createDashboard(this); }, enableSplitBy: function(fieldValues) { data.searchBar.splitBy['field'] = data.searchBar.splitByKeys.selected; @@ -467,6 +498,9 @@ Vue.component('search-bar', { var link = window.location.origin+ window.location.pathname + "?" + jQuery.param( params ); return link; + }, + dashboardActive: function (){ + return data.searchBar.splitByKeys.selected != ""; } }, template: ` @@ -573,13 +607,12 @@ Vue.component('search-bar', { title="Create Plot" v-on:click.prevent.stop="plot" > Plot - @@ -658,13 +691,15 @@ var data = { errorMessage: '', loadingGameActive: false }, + dashboard: { + tiles: [] + } }; function showLoadingIcon() { - data.resultView.imageUrl = ''; - data.resultView.errorMessage = ''; + hidePlotAndDashboard(); data.resultView.loadingGameActive = true; startInvaders(); @@ -676,26 +711,35 @@ function hideLoadingIcon() pauseInvaders(); } +function hidePlotAndDashboard() +{ + data.resultView.imageUrl = ''; + data.resultView.errorMessage = ''; + data.dashboard.tiles = []; +} + function plotCurrent() { showLoadingIcon(); if (data.searchBar.splitBy['field']) { - var query = createQuery(); + var splitBy = data.searchBar.splitBy; + var originalQuery = splitBy['query']; + var splitByField = splitBy['field']; + var splitByValue = splitBy['values'][splitBy['index']]; + var query = createQuery(originalQuery, splitByField, splitByValue); sendPlotRequest(query); }else{ sendPlotRequest(data.searchBar.query); } } -function createQuery() +function createQuery(query, splitByField, splitByValue) { - var splitBy = data.searchBar.splitBy; - var query = splitBy['query']; if (query.length > 0) { - query = "("+query+") and "+splitBy['field']+ " = " +splitBy['values'][splitBy['index']]; + query = "("+query+") and "+splitByField+ " = " +splitByValue; } else { - query = splitBy['field']+ " = " +splitBy['values'][splitBy['index']]; + query = splitByField+ " = " +splitByValue; } return query; } @@ -712,12 +756,11 @@ function groupBy() return result; } -function sendPlotRequest(query){ - +function createRequest(query){ var request = {}; request['query'] = query; - request['height'] = Math.floor($('#result-image').height()); - request['width'] = Math.floor($('#result-image').width()); + request['height'] = Math.floor($('#result').height()); + request['width'] = Math.floor($('#result').width()); request['groupBy'] = groupBy(); request['limitBy'] = data.searchBar.limitBy.selected; request['limit'] = parseInt(data.searchBar.limitBy.number); @@ -728,14 +771,19 @@ function sendPlotRequest(query){ request['aggregate'] = data.searchBar.aggregate; request['keyOutside'] = data.searchBar.keyOutside; + return request; +} + +function sendPlotRequest(query){ + const request = createRequest(query); - var success = function(response){ + const success = function(response){ data.resultView.imageUrl = response.imageUrl; data.resultView.errorMessage = ''; hideLoadingIcon(); updateImageLink(query); }; - var error = function(e) { + const error = function(e) { data.resultView.imageUrl = ''; if (e.status == 404){ data.resultView.errorMessage = "No data points found for query: " + query; @@ -770,13 +818,74 @@ function updateImageLink(query) { 'plotType': data.searchBar.plotType, 'aggregate': data.searchBar.aggregate, 'keyOutside': data.searchBar.keyOutside, - 'width': Math.floor($('#result-image').width()), - 'height': Math.floor($('#result-image').height()) + 'width': Math.floor($('#result').width()), + 'height': Math.floor($('#result').height()) }; data.searchBar.imagelink = window.location.origin+ window.location.pathname + "plots?" + jQuery.param(params); } +function createDashboard(vm){ + + hidePlotAndDashboard(); + + const imageHeight = Math.floor($('#app').height() - $('#search-bar').height() - $('#navigation').height()); + const imageWidth = Math.floor($('#app').width()- $('#search-bar').width() - $('#navigation').width()); + + const originalQuery = data.searchBar.query; + vm.splitQueries(function (fieldValues) { + var splitByField = data.searchBar.splitByKeys.selected; + + createDashboardItem(fieldValues, originalQuery, splitByField, imageHeight, imageWidth); + }); +} + + + + +function createDashboardItem(fieldValues, originalQuery, field, imageHeight, imageWidth) +{ + if (fieldValues.length > 0) { + var fieldValue = fieldValues.pop(); + const query = createQuery(originalQuery, field, fieldValue); + const request = createRequest(query); + request['height'] = imageHeight; + request['width'] = imageWidth; + request['thumbnailMaxWidth'] = 300; + request['thumbnailMaxHeight'] = 200; + + const success = function(response){ + data.dashboard.tiles.push({ + fieldValue: fieldValue, + thumbnailUrl: response.thumbnailUrl, + imageUrl: response.imageUrl + }); + createDashboardItem(fieldValues, originalQuery, field, imageHeight, imageWidth); + }; + const error = function(e) { + var errorMessage = ''; + if (e.status == 404){ + // skip + } + else if (e.status == 503){ + errorMessage = "Too many parallel requests."; + } + else{ + errorMessage = "FAILED: " + JSON.parse(e.responseText).message; + } + hideLoadingIcon(); + if (errorMessage) { + data.dashboard.tiles.push({ + fieldValue: fieldValue, + error: errorMessage + }); + } + createDashboardItem(fieldValues, originalQuery, field, imageHeight, imageWidth); + }; + postJson("plots", request, success, error); + } +} + function postJson(url, requestData, successCallback, errorCallback) { $.ajax({ diff --git a/pdb-ui/src/main/resources/templates/main.mustache b/pdb-ui/src/main/resources/templates/main.mustache index 6a78da7..988f68a 100644 --- a/pdb-ui/src/main/resources/templates/main.mustache +++ b/pdb-ui/src/main/resources/templates/main.mustache @@ -19,7 +19,10 @@
- +
+ + +
\ No newline at end of file