#include "histogram_controller.h" #include "../shared/poincare_helpers.h" #include "../shared/text_helpers.h" #include "app.h" #include #include #include #include #include #include using namespace Poincare; using namespace Shared; namespace Statistics { HistogramController::HistogramController(Responder * parentResponder, InputEventHandlerDelegate * inputEventHandlerDelegate, ButtonRowController * header, Store * store, uint32_t * storeVersion, uint32_t * barVersion, uint32_t * rangeVersion, int * selectedBarIndex, int * selectedSeriesIndex) : MultipleDataViewController(parentResponder, store, selectedBarIndex, selectedSeriesIndex), ButtonRowDelegate(header, nullptr), m_view(this, store), m_storeVersion(storeVersion), m_barVersion(barVersion), m_rangeVersion(rangeVersion), m_histogramParameterController(nullptr, inputEventHandlerDelegate, store) { } void HistogramController::setCurrentDrawnSeries(int series) { initYRangeParameters(series); /* The histogram's CurveView range has been updated along the Vertical axis. * To call drawLabelsAndGraduations (in HistogramView::drawRect()), the * CurveView must be reloaded so that labels and their values match the new * range. * In this situation, we update CurveView's Vertical axis, and draw horizontal * labels, which are independent. To avoid having to call CurveView::reload(), * axis could be taken into account when checking if labels are up to date, * instead of using rangeChecksum(), which mixes all axis. */ m_view.dataViewAtIndex(series)->CurveView::reload(); } StackViewController * HistogramController::stackController() { StackViewController * stack = (StackViewController *)(parentResponder()->parentResponder()->parentResponder()); return stack; } const char * HistogramController::title() { return I18n::translate(I18n::Message::HistogramTab); } bool HistogramController::handleEvent(Ion::Events::Event event) { assert(selectedSeriesIndex() >= 0); if (event == Ion::Events::OK || event == Ion::Events::EXE) { stackController()->push(histogramParameterController()); return true; } return MultipleDataViewController::handleEvent(event); } void HistogramController::viewWillAppear() { MultipleDataViewController::viewWillAppear(); uint32_t storeChecksum = m_store->storeChecksum(); bool initedRangeParameters = false; if (*m_storeVersion != storeChecksum) { *m_storeVersion = storeChecksum; initBarParameters(); initRangeParameters(); initedRangeParameters = true; } uint32_t barChecksum = m_store->barChecksum(); if (*m_barVersion != barChecksum) { *m_barVersion = barChecksum; if (!initedRangeParameters) { initRangeParameters(); } } uint32_t rangeChecksum = m_store->rangeChecksum(); if (*m_rangeVersion != rangeChecksum) { *m_rangeVersion = rangeChecksum; initBarSelection(); reloadBannerView(); } } void HistogramController::willExitResponderChain(Responder * nextFirstResponder) { if (nextFirstResponder == tabController()) { assert(tabController() != nullptr); if (selectedSeriesIndex() >= 0) { m_view.dataViewAtIndex(selectedSeriesIndex())->setForceOkDisplay(false); } } MultipleDataViewController::willExitResponderChain(nextFirstResponder); } void HistogramController::highlightSelection() { HistogramView * selectedHistogramView = static_cast(m_view.dataViewAtIndex(selectedSeriesIndex())); selectedHistogramView->setHighlight(m_store->startOfBarAtIndex(selectedSeriesIndex(), *m_selectedBarIndex), m_store->endOfBarAtIndex(selectedSeriesIndex(), *m_selectedBarIndex)); // if the selectedBar was outside of range, we need to scroll if (m_store->scrollToSelectedBarIndex(selectedSeriesIndex(), *m_selectedBarIndex)) { multipleDataView()->reload(); } } Responder * HistogramController::tabController() const { return (parentResponder()->parentResponder()->parentResponder()->parentResponder()); } void HistogramController::reloadBannerView() { if (selectedSeriesIndex() < 0) { return; } constexpr int precision = Preferences::LargeNumberOfSignificantDigits; constexpr size_t bufferSize = k_maxNumberOfCharacters + 2 * PrintFloat::charSizeForFloatsWithPrecision(precision); char buffer[bufferSize] = ""; int numberOfChar = 0; // Add Interval Data const char * legend = " ["; int legendLength = strlen(legend); strlcpy(buffer, legend, bufferSize); numberOfChar += legendLength; // Add lower bound if (selectedSeriesIndex() >= 0) { double lowerBound = m_store->startOfBarAtIndex(selectedSeriesIndex(), *m_selectedBarIndex); numberOfChar += PoincareHelpers::ConvertFloatToText(lowerBound, buffer+numberOfChar, bufferSize-numberOfChar, precision); } numberOfChar+= UTF8Decoder::CodePointToChars(';', buffer + numberOfChar, bufferSize - numberOfChar); // Add upper bound if (selectedSeriesIndex() >= 0) { double upperBound = m_store->endOfBarAtIndex(selectedSeriesIndex(), *m_selectedBarIndex); numberOfChar += PoincareHelpers::ConvertFloatToText(upperBound, buffer+numberOfChar, bufferSize-numberOfChar, precision); } numberOfChar+= UTF8Decoder::CodePointToChars('[', buffer + numberOfChar, bufferSize - numberOfChar); // Padding Shared::TextHelpers::PadWithSpaces(buffer, bufferSize, &numberOfChar, k_maxIntervalLegendLength); m_view.bannerView()->intervalView()->setText(buffer); // Add Size Data numberOfChar = 0; double size = 0; if (selectedSeriesIndex() >= 0) { size = m_store->heightOfBarAtIndex(selectedSeriesIndex(), *m_selectedBarIndex); numberOfChar += PoincareHelpers::ConvertFloatToText(size, buffer+numberOfChar, bufferSize-numberOfChar, precision); } // Padding Shared::TextHelpers::PadWithSpaces(buffer, bufferSize, &numberOfChar, k_maxLegendLength); m_view.bannerView()->sizeView()->setText(buffer); // Add Frequency Data numberOfChar = 0; if (selectedSeriesIndex() >= 0) { double frequency = size/m_store->sumOfOccurrences(selectedSeriesIndex()); numberOfChar += PoincareHelpers::ConvertFloatToText(frequency, buffer+numberOfChar, bufferSize - numberOfChar, precision); } // Padding Shared::TextHelpers::PadWithSpaces(buffer, bufferSize, &numberOfChar, k_maxLegendLength); m_view.bannerView()->frequencyView()->setText(buffer); m_view.bannerView()->reload(); } bool HistogramController::moveSelectionHorizontally(int deltaIndex) { int newSelectedBarIndex = *m_selectedBarIndex; do { newSelectedBarIndex+=deltaIndex; } while (m_store->heightOfBarAtIndex(selectedSeriesIndex(), newSelectedBarIndex) == 0 && newSelectedBarIndex >= 0 && newSelectedBarIndex < m_store->numberOfBars(selectedSeriesIndex())); if (newSelectedBarIndex >= 0 && newSelectedBarIndex < m_store->numberOfBars(selectedSeriesIndex()) && *m_selectedBarIndex != newSelectedBarIndex) { *m_selectedBarIndex = newSelectedBarIndex; m_view.dataViewAtIndex(selectedSeriesIndex())->setHighlight(m_store->startOfBarAtIndex(selectedSeriesIndex(), *m_selectedBarIndex), m_store->endOfBarAtIndex(selectedSeriesIndex(), *m_selectedBarIndex)); if (m_store->scrollToSelectedBarIndex(selectedSeriesIndex(), *m_selectedBarIndex)) { multipleDataView()->reload(); } reloadBannerView(); return true; } return false; } void HistogramController::preinitXRangeParameters(double * xMin) { /* Compute m_store's min and max values, hold them temporarily in the * CurveViewRange, for later use by initRangeParameters and * initBarParameters. Indeed, initRangeParameters will anyway alter the * CurveViewRange. The CurveViewRange setter methods take care of the case * where minValue >= maxValue. Moreover they compute the xGridUnit, which is * used by initBarParameters. */ double minValue = DBL_MAX; double maxValue = -DBL_MAX; for (int i = 0; i < Store::k_numberOfSeries; i ++) { if (!m_store->seriesIsEmpty(i)) { minValue = std::min(minValue, m_store->minValue(i)); maxValue = std::max(maxValue, m_store->maxValue(i)); } } assert(xMin != nullptr); *xMin = minValue; m_store->setXMin(minValue); m_store->setXMax(maxValue); } void HistogramController::initRangeParameters() { assert(selectedSeriesIndex() >= 0 && m_store->sumOfOccurrences(selectedSeriesIndex()) > 0); double barWidth = m_store->barWidth(); double xMin; preinitXRangeParameters(&xMin); double xMax = m_store->xMax() + barWidth; /* if a bar is represented by less than one pixel, we cap xMax */ if ((xMax - xMin)/barWidth > k_maxNumberOfBarsPerWindow) { xMax = xMin + k_maxNumberOfBarsPerWindow*barWidth; } m_store->setXMin(xMin - Store::k_displayLeftMarginRatio*(xMax-xMin)); m_store->setXMax(xMax + Store::k_displayRightMarginRatio*(xMax-xMin)); initYRangeParameters(selectedSeriesIndex()); } void HistogramController::initYRangeParameters(int series) { assert(series >= 0 && m_store->sumOfOccurrences(series) > 0); float yMax = -FLT_MAX; for (int index = 0; index < m_store->numberOfBars(series); index++) { float size = m_store->heightOfBarAtIndex(series, index); if (size > yMax) { yMax = size; } } yMax = yMax/m_store->sumOfOccurrences(series); yMax = yMax < 0 ? 1 : yMax; m_store->setYMax(yMax*(1.0f+Store::k_displayTopMarginRatio)); /* Compute YMin: * ratioFloatPixel*(0-yMin) = k_bottomMargin * ratioFloatPixel*(yMax-yMin) = viewHeight * * -ratioFloatPixel*yMin = k_bottomMargin * ratioFloatPixel*yMax-ratioFloatPixel*yMin = viewHeight * * ratioFloatPixel = (viewHeight - k_bottomMargin)/yMax * yMin = -k_bottomMargin/ratioFloatPixel = yMax*k_bottomMargin/(k_bottomMargin - viewHeight) * */ m_store->setYMin(m_store->yMax()*(float)Store::k_bottomMargin/((float)Store::k_bottomMargin - m_view.dataViewAtIndex(series)->bounds().height())); } void HistogramController::initBarParameters() { assert(selectedSeriesIndex() >= 0 && m_store->sumOfOccurrences(selectedSeriesIndex()) > 0); double xMin; preinitXRangeParameters(&xMin); m_store->setFirstDrawnBarAbscissa(xMin); double barWidth = m_store->xGridUnit(); if (barWidth <= 0.0) { barWidth = 1.0; } else { // Truncate the bar width, as we convert from float to double const double precision = 7; // TODO factorize? This is an experimental value, the same as in Expression;;Epsilon() const double logBarWidth = IEEE754::exponentBase10(barWidth); barWidth = ((int)(barWidth * std::pow(10.0, precision - logBarWidth))) * std::pow(10.0, -precision + logBarWidth); } m_store->setBarWidth(barWidth); } void HistogramController::initBarSelection() { assert(selectedSeriesIndex() >= 0 && m_store->sumOfOccurrences(selectedSeriesIndex()) > 0); *m_selectedBarIndex = 0; while ((m_store->heightOfBarAtIndex(selectedSeriesIndex(), *m_selectedBarIndex) == 0 || m_store->startOfBarAtIndex(selectedSeriesIndex(), *m_selectedBarIndex) < m_store->firstDrawnBarAbscissa()) && *m_selectedBarIndex < m_store->numberOfBars(selectedSeriesIndex())) { *m_selectedBarIndex = *m_selectedBarIndex+1; } if (*m_selectedBarIndex >= m_store->numberOfBars(selectedSeriesIndex())) { /* No bar is after m_firstDrawnBarAbscissa, so we select the first bar */ *m_selectedBarIndex = 0; while (m_store->heightOfBarAtIndex(selectedSeriesIndex(), *m_selectedBarIndex) == 0 && *m_selectedBarIndex < m_store->numberOfBars(selectedSeriesIndex())) { *m_selectedBarIndex = *m_selectedBarIndex+1; } } m_store->scrollToSelectedBarIndex(selectedSeriesIndex(), *m_selectedBarIndex); } }