From 15855fa84a09fd1fd486d357c38db0f2bd181e74 Mon Sep 17 00:00:00 2001 From: George Hazan Date: Tue, 4 Mar 2014 23:23:45 +0000 Subject: HistoryStats compiles ok now git-svn-id: http://svn.miranda-ng.org/main/trunk@8399 1316c22d-e87f-b044-9b9b-93d7a3e3ba9c --- plugins/HistoryStats/src/column_splittimeline.cpp | 638 ++++++++++++++++++++++ 1 file changed, 638 insertions(+) create mode 100644 plugins/HistoryStats/src/column_splittimeline.cpp (limited to 'plugins/HistoryStats/src/column_splittimeline.cpp') diff --git a/plugins/HistoryStats/src/column_splittimeline.cpp b/plugins/HistoryStats/src/column_splittimeline.cpp new file mode 100644 index 0000000000..f856167d04 --- /dev/null +++ b/plugins/HistoryStats/src/column_splittimeline.cpp @@ -0,0 +1,638 @@ +#include "_globals.h" +#include "column_splittimeline.h" + +/* + * ColSplitTimeline + */ + +ColSplitTimeline::ColSplitTimeline() + : m_nSource(0), m_nSourceType(2), m_nIgnoreOld(0), m_nVisMode(0), + m_nHODGroup(1), m_nDOWGroup(1), m_nBlockUnit(0), m_nUnitsPerBlock(6), + m_nBlocks(28), m_nGraphAlign(1), m_nCustomGroup(1), m_bTopPerColumn(true), + m_hSource(NULL), m_hIgnoreOld(NULL), m_hVisMode(NULL), + m_hHODGroup(NULL), m_hDOWGroup(NULL), m_hBlockUnit(NULL), m_hUnitsPerBlock(NULL), + m_hBlocks(NULL), m_hGraphAlign(NULL), m_hCustomGroup(NULL),m_hTopPerColumn(NULL), + m_nTimeDiv(3600), m_nTimeOffset(0), + m_nTimelineWidth(0), m_nBlockOffset(0), m_nNumBlocks(0) +{ +} + +void ColSplitTimeline::impl_copyConfig(const Column* pSource) +{ + const ColSplitTimeline& src = *reinterpret_cast(pSource); + + m_nSource = src.m_nSource; + m_nSourceType = src.m_nSourceType; + m_nIgnoreOld = src.m_nIgnoreOld; + m_nVisMode = src.m_nVisMode; + m_nHODGroup = src.m_nHODGroup; + m_nDOWGroup = src.m_nDOWGroup; + m_nBlockUnit = src.m_nBlockUnit; + m_nUnitsPerBlock = src.m_nUnitsPerBlock; + m_nBlocks = src.m_nBlocks; + m_nGraphAlign = src.m_nGraphAlign; + m_nCustomGroup = src.m_nCustomGroup; + m_bTopPerColumn = src.m_bTopPerColumn; +} + +void ColSplitTimeline::impl_configRead(const SettingsTree& settings) +{ + m_nSource = settings.readIntRanged(con::KeySource, 0, 0, 2); + m_nSourceType = settings.readIntRanged(con::KeySourceType, 2, 0, 3); + m_nIgnoreOld = settings.readIntRanged(con::KeyIgnoreOld, 0, 0, 1000); + m_nVisMode = settings.readIntRanged(con::KeyVisMode, 0, 0, 2); + m_nHODGroup = settings.readIntRanged(con::KeyHODGroup, 1, 1, 1000); + m_nDOWGroup = settings.readIntRanged(con::KeyDOWGroup, 1, 1, 1000); + m_nBlockUnit = settings.readIntRanged(con::KeyBlockUnit, 0, 0, 2); + m_nUnitsPerBlock = settings.readIntRanged(con::KeyUnitsPerBlock, 6, 1, 100); + m_nBlocks = settings.readIntRanged(con::KeyBlocks, 28, 1, 49); + m_nGraphAlign = settings.readIntRanged(con::KeyGraphAlign, 1, 0, 1); + m_nCustomGroup = settings.readIntRanged(con::KeyCustomGroup, 1, 1, 1000); + m_bTopPerColumn = settings.readBool(con::KeyTopPerColumn, true); +} + +void ColSplitTimeline::impl_configWrite(SettingsTree& settings) const +{ + settings.writeInt (con::KeySource, m_nSource); + settings.writeInt (con::KeySourceType, m_nSourceType); + settings.writeInt (con::KeyIgnoreOld, m_nIgnoreOld); + settings.writeInt (con::KeyVisMode, m_nVisMode); + settings.writeInt (con::KeyHODGroup, m_nHODGroup); + settings.writeInt (con::KeyDOWGroup, m_nDOWGroup); + settings.writeInt (con::KeyBlockUnit, m_nBlockUnit); + settings.writeInt (con::KeyUnitsPerBlock, m_nUnitsPerBlock); + settings.writeInt (con::KeyBlocks, m_nBlocks); + settings.writeInt (con::KeyGraphAlign, m_nGraphAlign); + settings.writeInt (con::KeyCustomGroup, m_nCustomGroup); + settings.writeBool(con::KeyTopPerColumn, m_bTopPerColumn); +} + +void ColSplitTimeline::impl_configToUI(OptionsCtrl& Opt, OptionsCtrl::Item hGroup) +{ + OptionsCtrl::Group hTemp; + OptionsCtrl::Radio hTempRadio; + + /**/m_hSource = Opt.insertCombo(hGroup, i18n(muT("Data source"))); + /**/m_hIgnoreOld = Opt.insertEdit(hGroup, i18n(muT("Drop everything older than (days, 0=no limit)")), muT(""), OptionsCtrl::OCF_NUMBER); + /**/hTemp = Opt.insertGroup(hGroup, i18n(muT("\"Split\" type"))); + /**/ m_hVisMode = Opt.insertRadio(hTemp, NULL, i18n(muT("Hours of day")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK); + /**/ m_hHODGroup = Opt.insertEdit (m_hVisMode, i18n(muT("Number of days to group")), muT(""), OptionsCtrl::OCF_NUMBER); + /**/ hTempRadio = Opt.insertRadio(hTemp, m_hVisMode, i18n(muT("Days of week")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK); + /**/ m_hDOWGroup = Opt.insertEdit (hTempRadio, i18n(muT("Number of weeks to group")), muT(""), OptionsCtrl::OCF_NUMBER); + /**/ hTempRadio = Opt.insertRadio(hTemp, m_hVisMode, i18n(muT("Custom (for experts only)")), OptionsCtrl::OCF_DISABLECHILDSONUNCHECK); + /**/ hTemp = Opt.insertGroup(hTempRadio, i18n(muT("Column setup"))); + /**/ m_hBlockUnit = Opt.insertCombo(hTemp, i18n(muT("Block unit"))); + /**/ m_hUnitsPerBlock = Opt.insertEdit (hTemp, i18n(muT("Units per block")), muT(""), OptionsCtrl::OCF_NUMBER); + /**/ m_hBlocks = Opt.insertEdit (hTemp, i18n(muT("Blocks per column")), muT(""), OptionsCtrl::OCF_NUMBER); + /**/ hTemp = Opt.insertGroup(hTempRadio, i18n(muT("Graph alignment"))); + /**/ m_hGraphAlign = Opt.insertRadio(hTemp, NULL, i18n(muT("Align on day boundary"))); + /**/ Opt.insertRadio(hTemp, m_hGraphAlign, i18n(muT("Align on week boundary"))); + /**/ m_hCustomGroup = Opt.insertEdit (hTempRadio, i18n(muT("Number of columns to group")), muT(""), OptionsCtrl::OCF_NUMBER); + /**/m_hTopPerColumn = Opt.insertCheck(hGroup, i18n(muT("Calculate maximum per column (not per graph)"))); + + static const mu_text* sourceTexts[] = { + I18N(muT("Characters (incoming)")), + I18N(muT("Characters (outgoing)")), + I18N(muT("Characters (all)")), + I18N(muT("Characters (in/out ratio)")), + I18N(muT("Messages (incoming)")), + I18N(muT("Messages (outgoing)")), + I18N(muT("Messages (all)")), + I18N(muT("Messages (in/out ratio)")), + I18N(muT("Chats (incoming)")), + I18N(muT("Chats (outgoing)")), + I18N(muT("Chats (all)")), + I18N(muT("Chats (in/out ratio)")), + }; + + array_each_(i, sourceTexts) + { + Opt.addComboItem(m_hSource, i18n(sourceTexts[i])); + } + + static const mu_text* unitTexts[] = { + I18N(muT("Hours")), + I18N(muT("Days")), + I18N(muT("Weeks")), + }; + + array_each_(i, unitTexts) + { + Opt.addComboItem(m_hBlockUnit, i18n(unitTexts[i])); + } + + Opt.setComboSelected(m_hSource , 4 * m_nSource + m_nSourceType); + Opt.setEditNumber (m_hIgnoreOld , m_nIgnoreOld ); + Opt.setRadioChecked (m_hVisMode , m_nVisMode ); + Opt.setEditNumber (m_hHODGroup , m_nHODGroup ); + Opt.setEditNumber (m_hDOWGroup , m_nDOWGroup ); + Opt.setComboSelected(m_hBlockUnit , m_nBlockUnit ); + Opt.setEditNumber (m_hUnitsPerBlock, m_nUnitsPerBlock ); + Opt.setEditNumber (m_hBlocks , m_nBlocks ); + Opt.setRadioChecked (m_hGraphAlign , m_nGraphAlign ); + Opt.setEditNumber (m_hCustomGroup , m_nCustomGroup ); + Opt.checkItem (m_hTopPerColumn , m_bTopPerColumn ); +} + +void ColSplitTimeline::impl_configFromUI(OptionsCtrl& Opt) +{ + m_nSource = Opt.getComboSelected(m_hSource ) / 4; + m_nSourceType = Opt.getComboSelected(m_hSource ) % 4; + m_nIgnoreOld = Opt.getEditNumber (m_hIgnoreOld ); + m_nVisMode = Opt.getRadioChecked (m_hVisMode ); + m_nHODGroup = Opt.getEditNumber (m_hHODGroup ); + m_nDOWGroup = Opt.getEditNumber (m_hDOWGroup ); + m_nBlockUnit = Opt.getComboSelected(m_hBlockUnit ); + m_nUnitsPerBlock = Opt.getEditNumber (m_hUnitsPerBlock); + m_nBlocks = Opt.getEditNumber (m_hBlocks ); + m_nGraphAlign = Opt.getRadioChecked (m_hGraphAlign ); + m_nCustomGroup = Opt.getEditNumber (m_hCustomGroup ); + m_bTopPerColumn = Opt.isItemChecked (m_hTopPerColumn ); + + // ensure constraints + utils::ensureRange(m_nIgnoreOld, 0, 1000, 0); + utils::ensureRange(m_nHODGroup, 1, 1000, 1); + utils::ensureRange(m_nDOWGroup, 1, 1000, 1); + utils::ensureRange(m_nUnitsPerBlock, 1, 100, 6); + utils::ensureRange(m_nBlocks, 1, 49, 28); + utils::ensureRange(m_nCustomGroup, 1, 1000, 1); +} + +ext::string ColSplitTimeline::impl_contactDataGetUID() const +{ + SplitParams params = getParams(); + + return ext::str(ext::format(muT("splittimeline-|-|-|")) + % m_nSource + % params.hours_in_block + % params.alignment); +} + +void ColSplitTimeline::impl_contactDataBeginAcquire() +{ + SplitParams params = getParams(); + + m_nTimeDiv = 3600 * params.hours_in_block; + + if (params.alignment == 1) + { + DWORD dwOffset = 0; + tm offsetTM = *gmtime(reinterpret_cast(&dwOffset)); + + m_nTimeOffset = 86400 * ((offsetTM.tm_wday + 6) % 7); + } + else + { + m_nTimeOffset = 0; + } +} + +void ColSplitTimeline::impl_contactDataPrepare(Contact& contact) const +{ + TimelineMap* pData = new TimelineMap; + + contact.setSlot(contactDataSlotGet(), pData); +} + +void ColSplitTimeline::impl_contactDataFree(Contact& contact) const +{ + TimelineMap* pData = reinterpret_cast(contact.getSlot(contactDataSlotGet())); + + if (pData) + { + delete pData; + contact.setSlot(contactDataSlotGet(), NULL); + } +} + +void ColSplitTimeline::addToSlot(Contact& contact, bool bOutgoing, DWORD localTimestamp, int toAdd) +{ + if (toAdd > 0) + { + TimelineMap* pData = reinterpret_cast(contact.getSlot(contactDataSlotGet())); + + InOut& io = (*pData)[(localTimestamp + m_nTimeOffset) / m_nTimeDiv]; + + (bOutgoing ? io.out : io.in) += toAdd; + } +} + +void ColSplitTimeline::impl_contactDataAcquireMessage(Contact& contact, Message& msg) +{ + if (m_nSource == 0) + { + addToSlot(contact, msg.isOutgoing(), msg.getTimestamp(), msg.getLength()); + } + else if (m_nSource == 1) + { + addToSlot(contact, msg.isOutgoing(), msg.getTimestamp(), 1); + } +} + +void ColSplitTimeline::impl_contactDataAcquireChat(Contact& contact, bool bOutgoing, DWORD localTimestampStarted, DWORD duration) +{ + if (m_nSource == 2) + { + addToSlot(contact, bOutgoing, localTimestampStarted, 1); + } +} + +void ColSplitTimeline::impl_contactDataMerge(Contact& contact, const Contact& include) const +{ + TimelineMap* pData = reinterpret_cast(contact.getSlot(contactDataSlotGet())); + const TimelineMap* pIncData = reinterpret_cast(include.getSlot(contactDataSlotGet())); + + citer_each_(TimelineMap, i, *pIncData) + { + (*pData)[i->first] += i->second; + } +} + +void ColSplitTimeline::impl_outputRenderHeader(ext::ostream& tos, int row, int rowSpan) const +{ + static const mu_text* szTypeDesc[] = { + I18N(muT("Hours of day timeline")), + I18N(muT("Days of week timeline")), + I18N(muT("\"Split\" timeline")), + }; + + static const mu_text* szSourceDesc[] = { + I18N(muT("incoming characters")), + I18N(muT("outgoing characters")), + I18N(muT("all characters")), + I18N(muT("in/out ratio of characters")), + I18N(muT("incoming messages")), + I18N(muT("outgoing messages")), + I18N(muT("all messages")), + I18N(muT("in/out ratio of messages")), + I18N(muT("incoming chats")), + I18N(muT("outgoing chats")), + I18N(muT("all chats")), + I18N(muT("in/out ratio of chats")), + }; + + if (row == 1) + { + SplitParams params = getParams(); + ext::string strTitle = str(ext::kformat(i18n(muT("#{type} for #{data}"))) + % muT("#{type}") * i18n(szTypeDesc[params.effective_vis_mode]) + % muT("#{data}") * i18n(szSourceDesc[4 * m_nSource + m_nSourceType])); + + writeRowspanTD(tos, getCustomTitle(i18n(szTypeDesc[params.effective_vis_mode]), strTitle) + muT("
"), row, 1, rowSpan); + } +} + +void ColSplitTimeline::impl_columnDataAfterOmit() +{ + // AFTER, i.e. contacts are trimmed to what user will see + + // init columns that are active but not acquireing + impl_contactDataBeginAcquire(); + + // put _all_ available contacts (including omitted/total) in one list + Statistic::ContactListC l; + + upto_each_(i, getStatistic()->countContacts()) + { + l.push_back(&getStatistic()->getContact(i)); + } + + if (getStatistic()->hasOmitted()) + { + l.push_back(&getStatistic()->getOmitted()); + } + + if (getStatistic()->hasTotals()) + { + l.push_back(&getStatistic()->getTotals()); + } + + SplitParams params = getParams(); + + if (l.size() > 0) + { + DWORD nFirstTime = con::MaxDateTime, nLastTime = con::MinDateTime; + + citer_each_(Statistic::ContactListC, c, l) + { + if ((*c)->isFirstLastTimeValid()) + { + nFirstTime = min(nFirstTime, (*c)->getFirstTime()); + nLastTime = max(nLastTime, (*c)->getLastTime()); + } + } + + if (nFirstTime == con::MaxDateTime && nLastTime == con::MinDateTime) + { + nFirstTime = nLastTime = 0; + } + + // honour ignore setting + if (m_nIgnoreOld > 0 && nLastTime > m_nIgnoreOld * 86400 && nLastTime - nFirstTime > m_nIgnoreOld * 86400) + { + nFirstTime = nLastTime - m_nIgnoreOld * 86400; + } + + // always align to day boundary + nFirstTime = (nFirstTime / 86400) * 86400; + + if (params.alignment == 1) + { + // align on week boundary + nFirstTime = ((nFirstTime + m_nTimeOffset) / 604800) * 604800 - m_nTimeOffset; + } + + // correct with "time offset" + nFirstTime += m_nTimeOffset; + nLastTime += m_nTimeOffset; + + m_nBlockOffset = nFirstTime / m_nTimeDiv; + m_nNumBlocks = nLastTime / m_nTimeDiv - m_nBlockOffset + 1; + } + else + { + m_nBlockOffset = m_nNumBlocks = 0; + } + + m_nTimelineWidth = 3 * ((m_nNumBlocks + params.blocks_in_column * params.columns_to_group - 1) / (params.blocks_in_column * params.columns_to_group)); +} + +void ColSplitTimeline::impl_outputRenderRow(ext::ostream& tos, const Contact& contact, DisplayType display) +{ + if (m_nSourceType != 3) + { + outputRenderRowInOut(tos, contact, display); + } + else + { + outputRenderRowRatio(tos, contact, display); + } +} + +ColSplitTimeline::SplitParams ColSplitTimeline::getParams() const +{ + static const int unitFactors[] = { 1, 24, 168 }; + + SplitParams params; + + params.effective_vis_mode = m_nVisMode; + + switch (m_nVisMode) + { + case 0: // hours of day + { + params.alignment = 0; + params.columns_to_group = m_nHODGroup; + params.hours_in_block = 1; + params.blocks_in_column = 24; + } + break; + + case 1: // days of week + { + params.alignment = 1; + params.columns_to_group = m_nDOWGroup; + params.hours_in_block = 24; + params.blocks_in_column = 7; + } + break; + + case 2: // custom + { + params.alignment = m_nGraphAlign; + params.columns_to_group = m_nCustomGroup; + params.hours_in_block = unitFactors[m_nBlockUnit] * m_nUnitsPerBlock; + params.blocks_in_column = m_nBlocks; + + // heuristics for custom mode + if (params.hours_in_block == 1 && params.blocks_in_column == 24) + { + params.effective_vis_mode = 0; + } + else if (params.hours_in_block == 24 && params.blocks_in_column == 7 && params.alignment == 1) + { + params.effective_vis_mode = 1; + } + } + break; + } + + return params; +} + +void ColSplitTimeline::outputRenderRowInOut(ext::ostream& tos, const Contact& contact, DisplayType display) +{ + SplitParams params = getParams(); + const TimelineMap* pData = reinterpret_cast(contact.getSlot(contactDataSlotGet())); + + int top = 0; + + if (!m_bTopPerColumn) + { + // calculate a global maximum + for (int curBlock = 0; curBlock < m_nNumBlocks; curBlock += params.blocks_in_column * params.columns_to_group) + { + for (int partBlock = 0; partBlock < params.blocks_in_column; ++partBlock) + { + int part_top = 0; + DWORD block_time = m_nBlockOffset + curBlock + partBlock; + + for (int curCol = 0; curCol < params.columns_to_group; ++curCol) + { + DWORD cur_time = block_time + curCol * params.blocks_in_column; + + TimelineMap::const_iterator i = pData->find(cur_time); + + if (i != pData->end()) + { + part_top += getValue(i->second); + } + } + + top = max(top, part_top); + } + } + + if (top == 0) + { + top = 1; + } + } + + tos << muT(""); + + // draw graph + Canvas canvas(m_nTimelineWidth, 49); + + canvas.fillBackground(con::ColorBack); + + COLORREF colorTab[256]; + + utils::generateGradient(con::ColorBack, con::ColorBar, colorTab); + + HDC hDC = canvas.beginDraw(); + + for (int curBlock = 0; curBlock < m_nNumBlocks; curBlock += params.blocks_in_column * params.columns_to_group) + { + int from_left = 3 * curBlock / (params.blocks_in_column * params.columns_to_group); + + if (m_bTopPerColumn) + { + // calculate a local maximum (per column) + top = 0; + + for (int partBlock = 0; partBlock < params.blocks_in_column; ++partBlock) + { + int part_top = 0; + DWORD block_time = m_nBlockOffset + curBlock + partBlock; + + for (int curCol = 0; curCol < params.columns_to_group; ++curCol) + { + DWORD cur_time = block_time + curCol * params.blocks_in_column; + + TimelineMap::const_iterator i = pData->find(cur_time); + + if (i != pData->end()) + { + part_top += getValue(i->second); + } + } + + top = max(top, part_top); + } + + if (top == 0) + { + top = 1; + } + } + + for (int partBlock = 0; partBlock < params.blocks_in_column; ++partBlock) + { + int part_top = 0; + DWORD block_time = m_nBlockOffset + curBlock + partBlock; + + for (int curCol = 0; curCol < params.columns_to_group; ++curCol) + { + DWORD cur_time = block_time + curCol * params.blocks_in_column; + + TimelineMap::const_iterator i = pData->find(cur_time); + + if (i != pData->end()) + { + part_top += getValue(i->second); + } + } + + if (part_top != 0) + { + RECT r = { + from_left, + 49 * partBlock / params.blocks_in_column, + from_left + 3, + 49 * (partBlock + 1) / params.blocks_in_column + }; + + int color = 255 * part_top / top; + + SetBkColor(hDC, colorTab[color]); + ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &r, NULL, 0, NULL); + } + } + } + + canvas.endDraw(); + + // write PNG file + ext::string strFinalFile; + + if (getStatistic()->newFilePNG(canvas, strFinalFile)) + { + tos << muT(""); + } + + tos << muT("") << ext::endl; +} + +void ColSplitTimeline::outputRenderRowRatio(ext::ostream& tos, const Contact& contact, DisplayType display) +{ + SplitParams params = getParams(); + const TimelineMap* pData = reinterpret_cast(contact.getSlot(contactDataSlotGet())); + + tos << muT(""); + + // draw graph + Canvas canvas(m_nTimelineWidth, 49); + + canvas.fillBackground(con::ColorBack); + + COLORREF inColorTab[256], outColorTab[256]; + + utils::generateGradient(con::ColorBack, con::ColorIn, inColorTab); + utils::generateGradient(con::ColorBack, con::ColorOut, outColorTab); + + HDC hDC = canvas.beginDraw(); + + for (int curBlock = 0; curBlock < m_nNumBlocks; curBlock += params.blocks_in_column * params.columns_to_group) + { + int from_left = 3 * curBlock / (params.blocks_in_column * params.columns_to_group); + + for (int partBlock = 0; partBlock < params.blocks_in_column; ++partBlock) + { + int part_in = 0; + int part_out = 0; + DWORD block_time = m_nBlockOffset + curBlock + partBlock; + + for (int curCol = 0; curCol < params.columns_to_group; ++curCol) + { + DWORD cur_time = block_time + curCol * params.blocks_in_column; + + TimelineMap::const_iterator i = pData->find(cur_time); + + if (i != pData->end()) + { + part_in += i->second.in; + part_out += i->second.out; + } + } + + int part_sum = part_in + part_out; + + if (part_sum != 0) + { + RECT r = { + from_left, + 49 * partBlock / params.blocks_in_column, + from_left + 3, + 49 * (partBlock + 1) / params.blocks_in_column + }; + + COLORREF color = inColorTab[0]; + + if (part_in > part_out) + { + color = inColorTab[255 * (2 * part_in - part_sum) / part_sum]; + } + else if (part_in < part_out) + { + color = outColorTab[255 * (2 * part_out - part_sum) / part_sum]; + } + + SetBkColor(hDC, color); + ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &r, NULL, 0, NULL); + } + } + } + + canvas.endDraw(); + + // write PNG file + ext::string strFinalFile; + + if (getStatistic()->newFilePNG(canvas, strFinalFile)) + { + tos << muT(""); + } + + tos << muT("") << ext::endl; +} -- cgit v1.2.3