Scaling big plots to small thumbnails results in bad images that barely show any details. We solve this by calling gnuplot a second time to generate the thumbnails. They don't have any labels and are rendered in the required size, so that not scaling is necessary. Thumbnails have to be requested explicitly, because it can be expensive to compute them.
276 lines
10 KiB
Java
276 lines
10 KiB
Java
package org.lucares.pdbui;
|
|
|
|
import java.io.FileInputStream;
|
|
import java.io.IOException;
|
|
import java.io.OutputStream;
|
|
import java.text.Collator;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.SortedSet;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.locks.ReentrantLock;
|
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
import org.lucares.pdb.datastore.Proposal;
|
|
import org.lucares.pdb.plot.api.AxisScale;
|
|
import org.lucares.pdb.plot.api.Limit;
|
|
import org.lucares.pdb.plot.api.PlotSettings;
|
|
import org.lucares.pdb.plot.api.PlotType;
|
|
import org.lucares.pdbui.domain.Aggregate;
|
|
import org.lucares.pdbui.domain.AutocompleteProposal;
|
|
import org.lucares.pdbui.domain.AutocompleteProposalByValue;
|
|
import org.lucares.pdbui.domain.AutocompleteResponse;
|
|
import org.lucares.pdbui.domain.PlotRequest;
|
|
import org.lucares.pdbui.domain.PlotResponse;
|
|
import org.lucares.pdbui.domain.PlotResponseStats;
|
|
import org.lucares.performance.db.PerformanceDb;
|
|
import org.lucares.recommind.logs.InternalPlottingException;
|
|
import org.lucares.recommind.logs.NoDataPointsException;
|
|
import org.lucares.recommind.logs.PlotResult;
|
|
import org.lucares.recommind.logs.Plotter;
|
|
import org.lucares.utils.CollectionUtils;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
|
import org.springframework.http.CacheControl;
|
|
import org.springframework.http.MediaType;
|
|
import org.springframework.http.ResponseEntity;
|
|
import org.springframework.stereotype.Controller;
|
|
import org.springframework.util.StreamUtils;
|
|
import org.springframework.web.bind.annotation.GetMapping;
|
|
import org.springframework.web.bind.annotation.PathVariable;
|
|
import org.springframework.web.bind.annotation.RequestBody;
|
|
import org.springframework.web.bind.annotation.RequestMapping;
|
|
import org.springframework.web.bind.annotation.RequestMethod;
|
|
import org.springframework.web.bind.annotation.RequestParam;
|
|
import org.springframework.web.bind.annotation.ResponseBody;
|
|
import org.springframework.web.servlet.ModelAndView;
|
|
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
|
|
|
import com.fasterxml.jackson.core.JsonParseException;
|
|
import com.fasterxml.jackson.databind.JsonMappingException;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
@Controller
|
|
@EnableAutoConfiguration
|
|
public class PdbController implements HardcodedValues, PropertyKeys {
|
|
|
|
private static final Logger LOGGER = LoggerFactory.getLogger(PdbController.class);
|
|
|
|
private final Plotter plotter;
|
|
private final PerformanceDb db;
|
|
|
|
private final ReentrantLock plotterLock = new ReentrantLock();
|
|
|
|
@Value("${" + PRODUCTION_MODE + ":true}")
|
|
private boolean modeProduction;
|
|
|
|
@Value("${" + CACHE_IMAGES_DURATION_SECONDS + ":" + CACHE_IMAGES_DURATION_SECONDS_DEFAULT + "}")
|
|
private int cacheDurationInSeconds;
|
|
|
|
public PdbController(final PerformanceDb db, final Plotter plotter) {
|
|
this.db = db;
|
|
this.plotter = plotter;
|
|
}
|
|
|
|
@GetMapping("/")
|
|
public ModelAndView index() {
|
|
final String view = "main";
|
|
final Map<String, Object> model = new HashMap<>();
|
|
// model.put("oldestValue",
|
|
// LocalDateTime.now().minusDays(7).format(DATE_FORMAT_BEGIN));
|
|
// model.put("latestValue", LocalDateTime.now().format(DATE_FORMAT_END));
|
|
model.put("isProduction", modeProduction);
|
|
return new ModelAndView(view, model);
|
|
}
|
|
|
|
@RequestMapping(path = "/plots", //
|
|
method = RequestMethod.GET, //
|
|
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, //
|
|
produces = MediaType.APPLICATION_JSON_UTF8_VALUE //
|
|
)
|
|
@ResponseBody
|
|
ResponseEntity<PlotResponse> createPlotGet(@RequestParam(name = "request") final String request)
|
|
throws InternalPlottingException, InterruptedException, JsonParseException, JsonMappingException,
|
|
IOException {
|
|
|
|
final ObjectMapper objectMapper = new ObjectMapper();
|
|
final PlotRequest plotRequest = objectMapper.readValue(request, PlotRequest.class);
|
|
|
|
return createPlot(plotRequest);
|
|
}
|
|
|
|
@RequestMapping(path = "/plots", //
|
|
method = RequestMethod.POST, //
|
|
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, //
|
|
produces = MediaType.APPLICATION_JSON_UTF8_VALUE //
|
|
)
|
|
@ResponseBody
|
|
ResponseEntity<PlotResponse> createPlot(@RequestBody final PlotRequest request)
|
|
throws InternalPlottingException, InterruptedException {
|
|
|
|
final PlotSettings plotSettings = PlotSettingsTransformer.toSettings(request);
|
|
if (StringUtils.isBlank(plotSettings.getQuery())) {
|
|
throw new BadRequest("The query must not be empty!");
|
|
}
|
|
|
|
// TODO the UI should cancel requests that are in flight before sending a plot
|
|
// request
|
|
if (plotterLock.tryLock(5, TimeUnit.SECONDS)) {
|
|
try {
|
|
final PlotResult result = plotter.plot(plotSettings);
|
|
|
|
final String imageUrl = WEB_IMAGE_OUTPUT_PATH + "/" + result.getImageName();
|
|
LOGGER.trace("image url: {}", imageUrl);
|
|
|
|
final String thumbnailUrl = result.getThumbnailPath() != null
|
|
? WEB_IMAGE_OUTPUT_PATH + "/" + result.getThumbnailName()
|
|
: "img/no-thumbnail.png";
|
|
|
|
final PlotResponseStats stats = PlotResponseStats.fromDataSeries(result.getDataSeries());
|
|
final PlotResponse plotResponse = new PlotResponse(stats, imageUrl, thumbnailUrl);
|
|
|
|
final CacheControl cacheControl = CacheControl.maxAge(cacheDurationInSeconds, TimeUnit.SECONDS);
|
|
|
|
return ResponseEntity.ok().cacheControl(cacheControl).body(plotResponse);
|
|
} catch (final NoDataPointsException e) {
|
|
throw new NotFoundException(e);
|
|
} finally {
|
|
plotterLock.unlock();
|
|
}
|
|
|
|
} else {
|
|
throw new ServiceUnavailableException("Too many parallel requests!");
|
|
}
|
|
}
|
|
|
|
@RequestMapping(path = "/plots", //
|
|
method = RequestMethod.GET, //
|
|
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE //
|
|
)
|
|
StreamingResponseBody createPlotImage(@RequestParam(name = "query", defaultValue = "") final String query,
|
|
@RequestParam(name = "groupBy[]", defaultValue = "") final List<String> aGroupBy,
|
|
@RequestParam(name = "limitBy.number", defaultValue = "10") final int limit,
|
|
@RequestParam(name = "limitBy.selected", defaultValue = "NO_LIMIT") final Limit limitBy,
|
|
@RequestParam(name = "dateFrom", defaultValue = "") final String dateFrom,
|
|
@RequestParam(name = "dateRange", defaultValue = "1 week") final String dateRange,
|
|
@RequestParam(name = "axisScale", defaultValue = "LINEAR") final AxisScale axisScale,
|
|
@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 = "width", defaultValue = "1920") final int hidth,
|
|
@RequestParam(name = "height", defaultValue = "1080") final int height) {
|
|
return (final OutputStream outputStream) -> {
|
|
|
|
if (StringUtils.isBlank(query)) {
|
|
throw new BadRequest("The query must not be empty!");
|
|
}
|
|
|
|
final PlotSettings plotSettings = new PlotSettings();
|
|
plotSettings.setQuery(query);
|
|
plotSettings.setGroupBy(aGroupBy);
|
|
plotSettings.setHeight(height);
|
|
plotSettings.setWidth(hidth);
|
|
plotSettings.setLimit(limit);
|
|
plotSettings.setLimitBy(limitBy);
|
|
plotSettings.setDateFrom(dateFrom);
|
|
plotSettings.setDateRange(dateRange);
|
|
plotSettings.setYAxisScale(axisScale);
|
|
plotSettings.setPlotType(plotType);
|
|
plotSettings.setAggregate(PlotSettingsTransformer.toAggregateInternal(aggregate));
|
|
plotSettings.setKeyOutside(keyOutside);
|
|
plotSettings.setGenerateThumbnail(false);
|
|
|
|
if (plotterLock.tryLock()) {
|
|
try {
|
|
final PlotResult result = plotter.plot(plotSettings);
|
|
|
|
try (FileInputStream in = new FileInputStream(result.getImagePath().toFile())) {
|
|
StreamUtils.copy(in, outputStream);
|
|
}
|
|
} catch (final NoDataPointsException e) {
|
|
throw new NotFoundException(e);
|
|
} catch (final InternalPlottingException e) {
|
|
throw new InternalServerError(e);
|
|
} finally {
|
|
plotterLock.unlock();
|
|
}
|
|
|
|
} else {
|
|
throw new ServiceUnavailableException("Too many parallel requests!");
|
|
}
|
|
};
|
|
}
|
|
|
|
@RequestMapping(path = "/autocomplete", //
|
|
method = RequestMethod.GET, //
|
|
produces = MediaType.APPLICATION_JSON_UTF8_VALUE //
|
|
)
|
|
@ResponseBody
|
|
AutocompleteResponse autocomplete(@RequestParam(name = "query") final String query,
|
|
@RequestParam(name = "caretIndex") final int caretIndex) {
|
|
|
|
final AutocompleteResponse result = new AutocompleteResponse();
|
|
final int zeroBasedCaretIndex = caretIndex - 1;
|
|
|
|
final List<Proposal> proposals = db.autocomplete(query, zeroBasedCaretIndex);
|
|
final List<Proposal> nonEmptyProposals = CollectionUtils.filter(proposals, p -> p.hasResults());
|
|
|
|
final List<AutocompleteProposal> autocompleteProposals = toAutocompleteProposals(nonEmptyProposals);
|
|
Collections.sort(autocompleteProposals, new AutocompleteProposalByValue());
|
|
|
|
result.setProposals(autocompleteProposals);
|
|
return result;
|
|
}
|
|
|
|
@RequestMapping(path = "/fields", //
|
|
method = RequestMethod.GET, //
|
|
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, //
|
|
produces = MediaType.APPLICATION_JSON_UTF8_VALUE //
|
|
)
|
|
@ResponseBody
|
|
List<String> fields() {
|
|
final List<String> fields = db.getFields();
|
|
|
|
fields.sort(Collator.getInstance(Locale.ENGLISH));
|
|
|
|
return fields;
|
|
}
|
|
|
|
@RequestMapping(path = "/fields/{fieldName}/values", //
|
|
method = RequestMethod.GET, //
|
|
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, //
|
|
produces = MediaType.APPLICATION_JSON_UTF8_VALUE //
|
|
)
|
|
@ResponseBody
|
|
SortedSet<String> fields(@PathVariable(name = "fieldName") final String fieldName,
|
|
@RequestParam(name = "query") final String query) {
|
|
|
|
final SortedSet<String> fields = db.getFieldsValues(query, fieldName);
|
|
|
|
return fields;
|
|
}
|
|
|
|
private List<AutocompleteProposal> toAutocompleteProposals(final List<Proposal> proposals) {
|
|
|
|
final List<AutocompleteProposal> result = new ArrayList<>();
|
|
|
|
for (final Proposal proposal : proposals) {
|
|
final AutocompleteProposal e = new AutocompleteProposal();
|
|
e.setValue(proposal.getProposedTag());
|
|
e.setNewQuery(proposal.getNewQuery());
|
|
e.setNewCaretPosition(proposal.getNewCaretPosition());
|
|
|
|
result.add(e);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
}
|