367 lines
12 KiB
C
367 lines
12 KiB
C
|
// =======================================================================
|
||
|
// GUIslice library (extensions)
|
||
|
// - Calvin Hass
|
||
|
// - https://www.impulseadventure.com/elec/guislice-gui.html
|
||
|
// - https://github.com/ImpulseAdventure/GUIslice
|
||
|
// =======================================================================
|
||
|
//
|
||
|
// The MIT License
|
||
|
//
|
||
|
// Copyright 2016-2020 Calvin Hass
|
||
|
//
|
||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
|
// of this software and associated documentation files (the "Software"), to deal
|
||
|
// in the Software without restriction, including without limitation the rights
|
||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
|
// copies of the Software, and to permit persons to whom the Software is
|
||
|
// furnished to do so, subject to the following conditions:
|
||
|
//
|
||
|
// The above copyright notice and this permission notice shall be included in
|
||
|
// all copies or substantial portions of the Software.
|
||
|
//
|
||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||
|
// THE SOFTWARE.
|
||
|
//
|
||
|
// =======================================================================
|
||
|
/// \file XGraph.c
|
||
|
|
||
|
|
||
|
|
||
|
// GUIslice library
|
||
|
#include "GUIslice.h"
|
||
|
#include "GUIslice_drv.h"
|
||
|
|
||
|
#include "XGraph.h"
|
||
|
|
||
|
#include <stdio.h>
|
||
|
|
||
|
#if (GSLC_USE_PROGMEM)
|
||
|
#include <avr/pgmspace.h>
|
||
|
#endif
|
||
|
|
||
|
// ----------------------------------------------------------------------------
|
||
|
// Error Messages
|
||
|
// ----------------------------------------------------------------------------
|
||
|
|
||
|
extern const char GSLC_PMEM ERRSTR_NULL[];
|
||
|
extern const char GSLC_PMEM ERRSTR_PXD_NULL[];
|
||
|
|
||
|
|
||
|
// ----------------------------------------------------------------------------
|
||
|
// Extended element definitions
|
||
|
// ----------------------------------------------------------------------------
|
||
|
//
|
||
|
// - This file extends the core GUIslice functionality with
|
||
|
// additional widget types
|
||
|
//
|
||
|
// ----------------------------------------------------------------------------
|
||
|
|
||
|
// ============================================================================
|
||
|
|
||
|
gslc_tsElemRef* gslc_ElemXGraphCreate(gslc_tsGui* pGui,int16_t nElemId,int16_t nPage,
|
||
|
gslc_tsXGraph* pXData,gslc_tsRect rElem,int16_t nFontId,int16_t* pBuf,
|
||
|
uint16_t nBufMax,gslc_tsColor colGraph)
|
||
|
{
|
||
|
if ((pGui == NULL) || (pXData == NULL)) {
|
||
|
static const char GSLC_PMEM FUNCSTR[] = "ElemXGraphCreate";
|
||
|
GSLC_DEBUG2_PRINT_CONST(ERRSTR_NULL,FUNCSTR);
|
||
|
return NULL;
|
||
|
}
|
||
|
gslc_tsElem sElem;
|
||
|
gslc_tsElemRef* pElemRef = NULL;
|
||
|
sElem = gslc_ElemCreate(pGui,nElemId,nPage,GSLC_TYPEX_GRAPH,rElem,NULL,0,nFontId);
|
||
|
sElem.nFeatures |= GSLC_ELEM_FEA_FRAME_EN;
|
||
|
sElem.nFeatures |= GSLC_ELEM_FEA_FILL_EN;
|
||
|
sElem.nFeatures &= ~GSLC_ELEM_FEA_CLICK_EN;
|
||
|
sElem.nFeatures &= ~GSLC_ELEM_FEA_GLOW_EN;
|
||
|
|
||
|
// Default group assignment. Can override later with ElemSetGroup()
|
||
|
sElem.nGroup = GSLC_GROUP_ID_NONE;
|
||
|
|
||
|
// Define other extended data
|
||
|
pXData->pBuf = pBuf;
|
||
|
pXData->nMargin = 5;
|
||
|
|
||
|
pXData->nBufMax = nBufMax;
|
||
|
pXData->nBufCnt = 0;
|
||
|
pXData->nPlotIndStart = 0;
|
||
|
|
||
|
pXData->colGraph = colGraph;
|
||
|
pXData->eStyle = GSLCX_GRAPH_STYLE_DOT;
|
||
|
|
||
|
// Define the visible region of the window
|
||
|
// - The range in value can be overridden by the user
|
||
|
pXData->nWndHeight = rElem.h - (2*pXData->nMargin);
|
||
|
pXData->nWndWidth = rElem.w - (2*pXData->nMargin);
|
||
|
|
||
|
// Default scale is
|
||
|
// - Each data point (buffer row) gets 1 pixel in X direction
|
||
|
// - Data value is directly mapped to height in Y direction
|
||
|
pXData->nPlotValMin = 0;
|
||
|
pXData->nPlotValMax = pXData->nWndHeight;
|
||
|
pXData->nPlotIndMax = pXData->nWndWidth;
|
||
|
|
||
|
|
||
|
// Clear the buffer
|
||
|
memset(pBuf,0,nBufMax*sizeof(int16_t));
|
||
|
|
||
|
|
||
|
// Determine if scrollbar should be enabled
|
||
|
if (pXData->nPlotIndMax >= pXData->nBufMax) {
|
||
|
// Disable scrollbar as the window is larger
|
||
|
// than the number of rows in the buffer
|
||
|
pXData->bScrollEn = false;
|
||
|
pXData->nScrollPos = 0;
|
||
|
} else {
|
||
|
// Scrollbar is enabled
|
||
|
pXData->bScrollEn = true;
|
||
|
pXData->nScrollPos = pXData->nBufMax - pXData->nPlotIndMax;
|
||
|
}
|
||
|
|
||
|
sElem.pXData = (void*)(pXData);
|
||
|
|
||
|
// Specify the custom drawing callback
|
||
|
sElem.pfuncXDraw = &gslc_ElemXGraphDraw;
|
||
|
sElem.pfuncXTouch = NULL;
|
||
|
sElem.colElemFill = GSLC_COL_BLACK;
|
||
|
sElem.colElemFillGlow = GSLC_COL_BLACK;
|
||
|
sElem.colElemFrame = GSLC_COL_GRAY;
|
||
|
sElem.colElemFrameGlow = GSLC_COL_WHITE;
|
||
|
if (nPage != GSLC_PAGE_NONE) {
|
||
|
pElemRef = gslc_ElemAdd(pGui,nPage,&sElem,GSLC_ELEMREF_DEFAULT);
|
||
|
return pElemRef;
|
||
|
#if (GSLC_FEATURE_COMPOUND)
|
||
|
} else {
|
||
|
// Save as temporary element
|
||
|
pGui->sElemTmp = sElem;
|
||
|
pGui->sElemRefTmp.pElem = &(pGui->sElemTmp);
|
||
|
pGui->sElemRefTmp.eElemFlags = GSLC_ELEMREF_DEFAULT | GSLC_ELEMREF_REDRAW_FULL;
|
||
|
return &(pGui->sElemRefTmp);
|
||
|
#endif
|
||
|
}
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
|
||
|
void gslc_ElemXGraphSetStyle(gslc_tsGui* pGui,gslc_tsElemRef* pElemRef,
|
||
|
gslc_teXGraphStyle eStyle,uint8_t nMargin)
|
||
|
{
|
||
|
gslc_tsXGraph* pBox;
|
||
|
gslc_tsElem* pElem = gslc_GetElemFromRef(pGui,pElemRef);
|
||
|
pBox = (gslc_tsXGraph*)(pElem->pXData);
|
||
|
|
||
|
pBox->eStyle = eStyle;
|
||
|
pBox->nMargin = nMargin;
|
||
|
|
||
|
// TODO: Recalculate the window extents and defaults
|
||
|
// - Note that ElemXGraphSetStyle() was not intended to be
|
||
|
// called after the control is already in use/displayed
|
||
|
|
||
|
// Set the redraw flag
|
||
|
// - Force full redraw
|
||
|
gslc_ElemSetRedraw(pGui,pElemRef,GSLC_REDRAW_FULL);
|
||
|
|
||
|
}
|
||
|
|
||
|
// TODO: Support scaling in X direction
|
||
|
void gslc_ElemXGraphSetRange(gslc_tsGui* pGui,gslc_tsElemRef* pElemRef,
|
||
|
int16_t nYMin,int16_t nYMax)
|
||
|
{
|
||
|
gslc_tsXGraph* pBox;
|
||
|
gslc_tsElem* pElem = gslc_GetElemFromRef(pGui,pElemRef);
|
||
|
pBox = (gslc_tsXGraph*)(pElem->pXData);
|
||
|
|
||
|
pBox->nPlotValMax = nYMax;
|
||
|
pBox->nPlotValMin = nYMin;
|
||
|
|
||
|
// Set the redraw flag
|
||
|
// - As we are changing the scale, force a full redraw
|
||
|
// since incremental redraws may make assumption about
|
||
|
// prior coordinate scaling
|
||
|
gslc_ElemSetRedraw(pGui,pElemRef,GSLC_REDRAW_FULL);
|
||
|
|
||
|
}
|
||
|
|
||
|
|
||
|
void gslc_ElemXGraphScrollSet(gslc_tsGui* pGui,gslc_tsElemRef* pElemRef,uint8_t nScrollPos,uint8_t nScrollMax)
|
||
|
{
|
||
|
|
||
|
gslc_tsXGraph* pBox;
|
||
|
gslc_tsElem* pElem = gslc_GetElemFromRef(pGui,pElemRef);
|
||
|
pBox = (gslc_tsXGraph*)(pElem->pXData);
|
||
|
|
||
|
// Ensure scrollbar is enabled
|
||
|
if (!pBox->bScrollEn) {
|
||
|
// Scrollbar is disabled, so ignore
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Assign proportional value based on visible window region
|
||
|
uint16_t nScrollPosOld = pBox->nScrollPos;
|
||
|
pBox->nScrollPos = nScrollPos * (pBox->nBufMax - pBox->nPlotIndMax) / nScrollMax;
|
||
|
|
||
|
// Set the redraw flag
|
||
|
// - Only need incremental redraw
|
||
|
// - Only redraw if changed actual scroll row
|
||
|
if (pBox->nScrollPos != nScrollPosOld) {
|
||
|
gslc_ElemSetRedraw(pGui,pElemRef,GSLC_REDRAW_INC);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
// Write a data value to the buffer
|
||
|
// - Advance the write ptr, wrap if needed
|
||
|
// - If encroach upon buffer read ptr, then drop the oldest line from the buffer
|
||
|
void gslc_ElemXGraphAdd(gslc_tsGui* pGui,gslc_tsElemRef* pElemRef,int16_t nVal)
|
||
|
{
|
||
|
gslc_tsXGraph* pBox = NULL;
|
||
|
gslc_tsElem* pElem = gslc_GetElemFromRef(pGui,pElemRef);
|
||
|
pBox = (gslc_tsXGraph*)(pElem->pXData);
|
||
|
|
||
|
// Add the data value
|
||
|
pBox->pBuf[pBox->nBufCnt] = nVal;
|
||
|
|
||
|
// Advance the pointer
|
||
|
// - Wrap the pointers around end of buffer
|
||
|
pBox->nBufCnt = (pBox->nBufCnt+1) % pBox->nBufMax;
|
||
|
|
||
|
// Set the redraw flag
|
||
|
// - Only need incremental redraw
|
||
|
gslc_ElemSetRedraw(pGui,pElemRef,GSLC_REDRAW_INC);
|
||
|
}
|
||
|
|
||
|
|
||
|
bool gslc_ElemXGraphDraw(void* pvGui,void* pvElemRef,gslc_teRedrawType eRedraw)
|
||
|
{
|
||
|
if ((pvGui == NULL) || (pvElemRef == NULL)) {
|
||
|
static const char GSLC_PMEM FUNCSTR[] = "ElemXGraphDraw";
|
||
|
GSLC_DEBUG2_PRINT_CONST(ERRSTR_NULL,FUNCSTR);
|
||
|
return false;
|
||
|
}
|
||
|
// Typecast the parameters to match the GUI and element types
|
||
|
gslc_tsGui* pGui = (gslc_tsGui*)(pvGui);
|
||
|
gslc_tsElemRef* pElemRef = (gslc_tsElemRef*)(pvElemRef);
|
||
|
gslc_tsElem* pElem = gslc_GetElemFromRef(pGui,pElemRef);
|
||
|
|
||
|
// Fetch the element's extended data structure
|
||
|
gslc_tsXGraph* pBox;
|
||
|
pBox = (gslc_tsXGraph*)(pElem->pXData);
|
||
|
if (pBox == NULL) {
|
||
|
GSLC_DEBUG2_PRINT("ERROR: ElemXGraphDraw(%s) pXData is NULL\n","");
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
bool bGlow = (pElem->nFeatures & GSLC_ELEM_FEA_GLOW_EN) && gslc_ElemGetGlow(pGui,pElemRef);
|
||
|
bool bFrameEn = (pElem->nFeatures & GSLC_ELEM_FEA_FRAME_EN);
|
||
|
|
||
|
// Draw the frame
|
||
|
if (eRedraw == GSLC_REDRAW_FULL) {
|
||
|
if (bFrameEn) {
|
||
|
gslc_DrawFrameRect(pGui,pElem->rElem,pElem->colElemFrame);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Clear the background (inset from frame)
|
||
|
// TODO: Support incremental redraw in which case we don't
|
||
|
// erase the inner region. Instead we would just erase
|
||
|
// old values and redraw new ones
|
||
|
gslc_tsRect rInner = gslc_ExpandRect(pElem->rElem,-1,-1);
|
||
|
gslc_DrawFillRect(pGui,rInner,(bGlow)?pElem->colElemFillGlow:pElem->colElemFill);
|
||
|
|
||
|
int16_t nDataVal;
|
||
|
uint16_t nCurX = 0;
|
||
|
int16_t nPixX,nPixY,nPixYBase,nPixYOffset;
|
||
|
gslc_tsColor colGraph;
|
||
|
|
||
|
uint8_t nScrollMax;
|
||
|
|
||
|
// Initialize color state
|
||
|
colGraph = pBox->colGraph;
|
||
|
|
||
|
// Calculate the current window position based on
|
||
|
// the current buffer write pointer and scroll
|
||
|
// position
|
||
|
nScrollMax = pBox->nBufMax - pBox->nPlotIndMax;
|
||
|
pBox->nPlotIndStart = pBox->nBufMax + pBox->nBufCnt;
|
||
|
pBox->nPlotIndStart -= pBox->nPlotIndMax;
|
||
|
// Only correct for scrollbar position if enabled
|
||
|
if (pBox->bScrollEn) {
|
||
|
pBox->nPlotIndStart -= (nScrollMax - pBox->nScrollPos);
|
||
|
}
|
||
|
pBox->nPlotIndStart = pBox->nPlotIndStart % pBox->nBufMax;
|
||
|
|
||
|
uint8_t nPlotInd = 0;
|
||
|
uint8_t nIndMax = 0;
|
||
|
nIndMax = (pBox->nBufMax < pBox->nPlotIndMax)? pBox->nBufMax : pBox->nPlotIndMax;
|
||
|
for (nPlotInd=0;nPlotInd<nIndMax;nPlotInd++) {
|
||
|
|
||
|
// Calculate row offset after accounting for buffer wrap
|
||
|
// and current window starting offset
|
||
|
uint16_t nBufInd = pBox->nPlotIndStart + nPlotInd;
|
||
|
nBufInd = nBufInd % pBox->nBufMax;
|
||
|
|
||
|
// NOTE: At the start of buffer fill when we have
|
||
|
// only written a few values, we will continue to read
|
||
|
// values out of the buffer so we are dependent upon
|
||
|
// the reset to initialize the buffer to zero.
|
||
|
|
||
|
nDataVal = pBox->pBuf[nBufInd];
|
||
|
|
||
|
// Clip the value to the plot range
|
||
|
if (nDataVal > pBox->nPlotValMax) { nDataVal = pBox->nPlotValMax; }
|
||
|
else if (nDataVal < pBox->nPlotValMin) { nDataVal = pBox->nPlotValMin; }
|
||
|
|
||
|
// TODO: Make nCurX a scaled version of nPlotInd
|
||
|
// - Support different modes for mapping multiple data points
|
||
|
// a single X coordinate and mapping a single data point
|
||
|
// to multiple X coordinates
|
||
|
// - For now, just have a 1:1 correspondence between data
|
||
|
// points and the X coordinate
|
||
|
nCurX = nPlotInd;
|
||
|
|
||
|
// TODO: Scale data value
|
||
|
|
||
|
// TODO: Consider supporting various color mapping modes
|
||
|
//colGraph = gslc_ColorBlend2(GSLC_COL_BLACK,GSLC_COL_WHITE,500,nDataVal*500/200);
|
||
|
|
||
|
// Determine the drawing coordinates
|
||
|
nPixX = pElem->rElem.x + pBox->nMargin + nCurX;
|
||
|
nPixYBase = pElem->rElem.y - pBox->nMargin + pElem->rElem.h-1;
|
||
|
|
||
|
// Calculate Y coordinate
|
||
|
nPixYOffset = nDataVal;
|
||
|
|
||
|
// Clip plot Y coordinate
|
||
|
if (nPixY > pBox->nWndHeight) { nPixY = pBox->nWndHeight; }
|
||
|
if (nPixY < 0) { nPixY = 0; }
|
||
|
|
||
|
// Calculate final Y coordinate
|
||
|
nPixY = pElem->rElem.y - pBox->nMargin + pElem->rElem.h-1 - nPixYOffset;
|
||
|
|
||
|
|
||
|
// Render the datapoints
|
||
|
if (pBox->eStyle == GSLCX_GRAPH_STYLE_DOT) {
|
||
|
gslc_DrawSetPixel(pGui,nPixX,nPixY,colGraph);
|
||
|
} else if (pBox->eStyle == GSLCX_GRAPH_STYLE_LINE) {
|
||
|
} else if (pBox->eStyle == GSLCX_GRAPH_STYLE_FILL) {
|
||
|
gslc_DrawLine(pGui,nPixX,nPixYBase,nPixX,nPixY,colGraph);
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
// Clear the redraw flag
|
||
|
gslc_ElemSetRedraw(pGui,pElemRef,GSLC_REDRAW_NONE);
|
||
|
|
||
|
// Mark page as needing flip
|
||
|
gslc_PageFlipSet(pGui,true);
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// ============================================================================
|