Chris@439: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ Chris@439: Chris@439: /* Chris@439: Sonic Visualiser Chris@439: An audio file viewer and annotation editor. Chris@439: Centre for Digital Music, Queen Mary, University of London. Chris@439: This file copyright 2008 QMUL. Chris@439: Chris@439: This program is free software; you can redistribute it and/or Chris@439: modify it under the terms of the GNU General Public License as Chris@439: published by the Free Software Foundation; either version 2 of the Chris@439: License, or (at your option) any later version. See the file Chris@439: COPYING included with this distribution for more information. Chris@439: */ Chris@439: Chris@439: #include "RDFImporter.h" Chris@439: Chris@439: #include Chris@439: #include Chris@439: Chris@439: #include Chris@439: #include Chris@439: Chris@439: #include "SimpleSPARQLQuery.h" Chris@439: Chris@439: #include "base/ProgressReporter.h" Chris@439: #include "base/RealTime.h" Chris@439: Chris@439: #include "data/model/SparseOneDimensionalModel.h" Chris@439: #include "data/model/SparseTimeValueModel.h" Chris@439: #include "data/model/EditableDenseThreeDimensionalModel.h" Chris@439: Chris@439: using std::cerr; Chris@439: using std::endl; Chris@439: Chris@439: class RDFImporterImpl Chris@439: { Chris@439: public: Chris@439: RDFImporterImpl(QString url, int sampleRate); Chris@439: virtual ~RDFImporterImpl(); Chris@439: Chris@439: bool isOK(); Chris@439: QString getErrorString() const; Chris@439: Chris@439: std::vector getDataModels(ProgressReporter *); Chris@439: Chris@439: protected: Chris@439: QString m_uristring; Chris@439: QString m_errorString; Chris@439: int m_sampleRate; Chris@439: Chris@439: typedef std::vector ValueList; Chris@439: typedef std::map TimeValueMap; Chris@439: typedef std::map TypeTimeValueMap; Chris@439: typedef std::map SourceTypeTimeValueMap; Chris@439: Chris@440: void getDataModelsSparse(std::vector &, ProgressReporter *); Chris@440: void getDataModelsDense(std::vector &, ProgressReporter *); Chris@440: Chris@440: void getDenseFeatureProperties(QString featureUri, Chris@440: int &sampleRate, int &windowLength, Chris@440: int &hopSize, int &width, int &height); Chris@440: Chris@439: void extractStructure(const TimeValueMap &map, bool &sparse, Chris@439: int &minValueCount, int &maxValueCount); Chris@439: Chris@439: void fillModel(SparseOneDimensionalModel *, const TimeValueMap &); Chris@439: void fillModel(SparseTimeValueModel *, const TimeValueMap &); Chris@439: void fillModel(EditableDenseThreeDimensionalModel *, const TimeValueMap &); Chris@439: }; Chris@439: Chris@439: Chris@439: QString Chris@439: RDFImporter::getKnownExtensions() Chris@439: { Chris@439: return "*.rdf *.n3 *.ttl"; Chris@439: } Chris@439: Chris@439: RDFImporter::RDFImporter(QString url, int sampleRate) : Chris@439: m_d(new RDFImporterImpl(url, sampleRate)) Chris@439: { Chris@439: } Chris@439: Chris@439: RDFImporter::~RDFImporter() Chris@439: { Chris@439: delete m_d; Chris@439: } Chris@439: Chris@439: bool Chris@439: RDFImporter::isOK() Chris@439: { Chris@439: return m_d->isOK(); Chris@439: } Chris@439: Chris@439: QString Chris@439: RDFImporter::getErrorString() const Chris@439: { Chris@439: return m_d->getErrorString(); Chris@439: } Chris@439: Chris@439: std::vector Chris@439: RDFImporter::getDataModels(ProgressReporter *r) Chris@439: { Chris@439: return m_d->getDataModels(r); Chris@439: } Chris@439: Chris@439: RDFImporterImpl::RDFImporterImpl(QString uri, int sampleRate) : Chris@439: m_uristring(uri), Chris@439: m_sampleRate(sampleRate) Chris@439: { Chris@439: } Chris@439: Chris@439: RDFImporterImpl::~RDFImporterImpl() Chris@439: { Chris@439: } Chris@439: Chris@439: bool Chris@439: RDFImporterImpl::isOK() Chris@439: { Chris@439: return (m_errorString == ""); Chris@439: } Chris@439: Chris@439: QString Chris@439: RDFImporterImpl::getErrorString() const Chris@439: { Chris@439: return m_errorString; Chris@439: } Chris@439: Chris@439: std::vector Chris@439: RDFImporterImpl::getDataModels(ProgressReporter *reporter) Chris@439: { Chris@439: std::vector models; Chris@439: Chris@440: getDataModelsDense(models, reporter); Chris@440: Chris@440: QString error; Chris@440: if (!isOK()) error = m_errorString; Chris@440: m_errorString = ""; Chris@440: Chris@440: getDataModelsSparse(models, reporter); Chris@440: Chris@440: if (isOK()) m_errorString = error; Chris@440: Chris@440: return models; Chris@440: } Chris@440: Chris@440: void Chris@440: RDFImporterImpl::getDataModelsDense(std::vector &models, Chris@440: ProgressReporter *reporter) Chris@440: { Chris@440: SimpleSPARQLQuery query = SimpleSPARQLQuery Chris@440: (QString Chris@440: ( Chris@440: " PREFIX mo: " Chris@440: " PREFIX af: " Chris@440: Chris@440: " SELECT ?feature ?signal_source ?feature_signal_type ?value " Chris@440: " FROM <%1> " Chris@440: Chris@440: " WHERE { " Chris@440: Chris@440: " ?signal a mo:Signal ; " Chris@440: " mo:available_as ?signal_source ; " Chris@440: " af:signal_feature ?feature . " Chris@440: Chris@440: " ?feature a ?feature_signal_type ; " Chris@440: " af:value ?value . " Chris@440: Chris@440: " } " Chris@440: ) Chris@440: .arg(m_uristring)); Chris@440: Chris@440: SimpleSPARQLQuery::ResultList results = query.execute(); Chris@440: Chris@440: if (!query.isOK()) { Chris@440: m_errorString = query.getErrorString(); Chris@440: return; Chris@440: } Chris@440: Chris@440: if (query.wasCancelled()) { Chris@440: m_errorString = "Query cancelled"; Chris@440: return; Chris@440: } Chris@440: Chris@440: for (int i = 0; i < results.size(); ++i) { Chris@440: Chris@440: QString feature = results[i]["feature"].value; Chris@440: QString source = results[i]["signal_source"].value; Chris@440: QString type = results[i]["feature_signal_type"].value; Chris@440: QString value = results[i]["value"].value; Chris@440: Chris@440: int sampleRate = 0; Chris@440: int windowLength = 0; Chris@440: int hopSize = 0; Chris@440: int width = 0; Chris@440: int height = 0; Chris@440: getDenseFeatureProperties Chris@440: (feature, sampleRate, windowLength, hopSize, width, height); Chris@440: Chris@440: if (sampleRate != 0 && sampleRate != m_sampleRate) { Chris@440: cerr << "WARNING: Sample rate in dense feature description does not match our underlying rate -- using rate from feature description" << endl; Chris@440: } Chris@440: if (sampleRate == 0) sampleRate = m_sampleRate; Chris@440: Chris@440: if (hopSize == 0) { Chris@440: cerr << "WARNING: Dense feature description does not specify a hop size -- assuming 1" << endl; Chris@440: hopSize = 1; Chris@440: } Chris@440: Chris@440: if (height == 0) { Chris@440: cerr << "WARNING: Dense feature description does not specify feature signal dimensions -- assuming one-dimensional (height = 1)" << endl; Chris@440: height = 1; Chris@440: } Chris@440: Chris@440: QStringList values = value.split(' ', QString::SkipEmptyParts); Chris@440: Chris@440: if (values.empty()) { Chris@440: cerr << "WARNING: Dense feature description does not specify any values!" << endl; Chris@440: continue; Chris@440: } Chris@440: Chris@440: if (height == 1) { Chris@440: Chris@440: SparseTimeValueModel *m = new SparseTimeValueModel Chris@440: (sampleRate, hopSize, false); Chris@440: Chris@440: for (int j = 0; j < values.size(); ++j) { Chris@440: float f = values[j].toFloat(); Chris@440: SparseTimeValueModel::Point point(j * hopSize, f, ""); Chris@440: m->addPoint(point); Chris@440: } Chris@440: Chris@440: models.push_back(m); Chris@440: Chris@440: } else { Chris@440: Chris@440: EditableDenseThreeDimensionalModel *m = Chris@440: new EditableDenseThreeDimensionalModel(sampleRate, hopSize, Chris@440: height, false); Chris@440: Chris@440: EditableDenseThreeDimensionalModel::Column column; Chris@440: Chris@440: int x = 0; Chris@440: Chris@440: for (int j = 0; j < values.size(); ++j) { Chris@440: if (j % height == 0 && !column.empty()) { Chris@440: m->setColumn(x++, column); Chris@440: column.clear(); Chris@440: } Chris@440: column.push_back(values[j].toFloat()); Chris@440: } Chris@440: Chris@440: if (!column.empty()) { Chris@440: m->setColumn(x++, column); Chris@440: } Chris@440: Chris@440: models.push_back(m); Chris@440: } Chris@440: } Chris@440: } Chris@440: Chris@440: void Chris@440: RDFImporterImpl::getDenseFeatureProperties(QString featureUri, Chris@440: int &sampleRate, int &windowLength, Chris@440: int &hopSize, int &width, int &height) Chris@440: { Chris@440: QString dimensionsQuery Chris@440: ( Chris@440: " PREFIX mo: " Chris@440: " PREFIX af: " Chris@440: Chris@440: " SELECT ?dimensions " Chris@440: " FROM <%1> " Chris@440: Chris@440: " WHERE { " Chris@440: Chris@440: " <%2> af:dimensions ?dimensions . " Chris@440: Chris@440: " } " Chris@440: ); Chris@440: Chris@440: SimpleSPARQLQuery::Value dimensionsValue = Chris@440: SimpleSPARQLQuery::singleResultQuery(dimensionsQuery Chris@440: .arg(m_uristring).arg(featureUri), Chris@440: "dimensions"); Chris@440: Chris@440: cerr << "Dimensions = \"" << dimensionsValue.value.toStdString() << "\"" Chris@440: << endl; Chris@440: Chris@440: if (dimensionsValue.value != "") { Chris@440: QStringList dl = dimensionsValue.value.split(" "); Chris@440: if (dl.empty()) dl.push_back(dimensionsValue.value); Chris@440: if (dl.size() > 0) height = dl[0].toInt(); Chris@440: if (dl.size() > 1) width = dl[1].toInt(); Chris@440: } Chris@440: Chris@440: QString queryTemplate Chris@440: ( Chris@440: " PREFIX mo: " Chris@440: " PREFIX af: " Chris@440: " PREFIX tl: " Chris@440: Chris@440: " SELECT ?%3 " Chris@440: " FROM <%1> " Chris@440: Chris@440: " WHERE { " Chris@440: Chris@440: " <%2> mo:time ?time . " Chris@440: Chris@440: " ?time a tl:Interval ; " Chris@440: " tl:onTimeLine ?timeline . " Chris@440: Chris@440: " ?map tl:rangeTimeLine ?timeline . " Chris@440: Chris@440: " ?map tl:%3 ?%3 . " Chris@440: Chris@440: " } " Chris@440: ); Chris@440: Chris@440: // Another laborious workaround for rasqal's failure to handle Chris@440: // multiple optionals properly Chris@440: Chris@440: SimpleSPARQLQuery::Value srValue = Chris@440: SimpleSPARQLQuery::singleResultQuery(queryTemplate Chris@440: .arg(m_uristring).arg(featureUri) Chris@440: .arg("sampleRate"), Chris@440: "sampleRate"); Chris@440: if (srValue.value != "") { Chris@440: sampleRate = srValue.value.toInt(); Chris@440: } Chris@440: Chris@440: SimpleSPARQLQuery::Value hopValue = Chris@440: SimpleSPARQLQuery::singleResultQuery(queryTemplate Chris@440: .arg(m_uristring).arg(featureUri) Chris@440: .arg("hopSize"), Chris@440: "hopSize"); Chris@440: if (srValue.value != "") { Chris@440: hopSize = hopValue.value.toInt(); Chris@440: } Chris@440: Chris@440: SimpleSPARQLQuery::Value winValue = Chris@440: SimpleSPARQLQuery::singleResultQuery(queryTemplate Chris@440: .arg(m_uristring).arg(featureUri) Chris@440: .arg("windowLength"), Chris@440: "windowLength"); Chris@440: if (winValue.value != "") { Chris@440: windowLength = winValue.value.toInt(); Chris@440: } Chris@440: Chris@440: cerr << "sr = " << sampleRate << ", hop = " << hopSize << ", win = " << windowLength << endl; Chris@440: } Chris@440: Chris@440: void Chris@440: RDFImporterImpl::getDataModelsSparse(std::vector &models, Chris@440: ProgressReporter *reporter) Chris@440: { Chris@439: // Our query is intended to retrieve every thing that has a time, Chris@439: // and every feature type and value associated with a thing that Chris@439: // has a time. Chris@439: Chris@439: // We will then need to refine this big bag of results into a set Chris@439: // of data models. Chris@439: Chris@439: // Results that have different source signals should go into Chris@439: // different models. Chris@439: Chris@439: // Results that have different feature types should go into Chris@439: // different models. Chris@439: Chris@439: // Results that are sparse should go into different models from Chris@439: // those that are dense (we need to examine the timestamps to Chris@439: // establish this -- if the timestamps are regular, the results Chris@439: // are dense -- so we can't do it as we go along, only after Chris@439: // collecting all results). Chris@439: Chris@439: // Timed things that have features associated with them should not Chris@439: // appear directly in any model -- their features should appear Chris@439: // instead -- and these should be different models from those used Chris@439: // for timed things that do not have features. Chris@439: Chris@439: // As we load the results, we'll push them into a partially Chris@439: // structured container that maps from source signal (URI as Chris@439: // string) -> feature type (likewise) -> time -> list of values. Chris@439: // If the source signal or feature type is unavailable, the empty Chris@439: // string will do. Chris@439: Chris@439: SourceTypeTimeValueMap m; Chris@439: Chris@439: QString queryString = QString( Chris@439: Chris@439: " PREFIX event: " Chris@439: " PREFIX time: " Chris@439: " PREFIX mo: " Chris@439: " PREFIX af: " Chris@439: Chris@440: " SELECT ?signal_source ?time ?event_type ?value" Chris@439: " FROM <%1>" Chris@439: Chris@439: " WHERE {" Chris@440: Chris@440: " ?signal mo:available_as ?signal_source ." Chris@440: " ?signal a mo:Signal ." Chris@440: Chris@439: " ?signal mo:time ?interval ." Chris@439: " ?interval time:onTimeLine ?tl ." Chris@439: " ?t time:onTimeLine ?tl ." Chris@439: " ?t time:at ?time ." Chris@440: " ?timed_thing event:time ?t ." Chris@440: " ?timed_thing a ?event_type ." Chris@440: Chris@439: " OPTIONAL {" Chris@440: " ?timed_thing af:feature ?value" Chris@439: " }" Chris@439: " }" Chris@439: Chris@439: ).arg(m_uristring); Chris@439: Chris@439: SimpleSPARQLQuery query(queryString); Chris@439: query.setProgressReporter(reporter); Chris@439: Chris@439: cerr << "Query will be: " << queryString.toStdString() << endl; Chris@439: Chris@439: SimpleSPARQLQuery::ResultList results = query.execute(); Chris@439: Chris@439: if (!query.isOK()) { Chris@439: m_errorString = query.getErrorString(); Chris@440: return; Chris@439: } Chris@439: Chris@439: if (query.wasCancelled()) { Chris@439: m_errorString = "Query cancelled"; Chris@440: return; Chris@439: } Chris@439: Chris@439: for (int i = 0; i < results.size(); ++i) { Chris@439: Chris@440: QString source = results[i]["signal_source"].value; Chris@439: Chris@448: RealTime time; Chris@439: QString timestring = results[i]["time"].value; Chris@439: time = RealTime::fromXsdDuration(timestring.toStdString()); Chris@439: cerr << "time = " << time.toString() << " (from xsd:duration \"" Chris@439: << timestring.toStdString() << "\")" << endl; Chris@439: Chris@440: QString type = results[i]["event_type"].value; Chris@439: Chris@439: QString valuestring = results[i]["value"].value; Chris@439: float value = 0.f; Chris@439: bool haveValue = false; Chris@439: if (valuestring != "") { Chris@440: //!!! no -- runner actually writes a "CSV literal" Chris@439: value = valuestring.toFloat(&haveValue); Chris@439: cerr << "value = " << value << endl; Chris@439: } Chris@439: Chris@439: if (haveValue) { Chris@439: m[source][type][time].push_back(value); Chris@439: } else if (m[source][type].find(time) == m[source][type].end()) { Chris@439: m[source][type][time] = ValueList(); Chris@439: } Chris@439: } Chris@439: Chris@439: for (SourceTypeTimeValueMap::const_iterator mi = m.begin(); Chris@439: mi != m.end(); ++mi) { Chris@439: Chris@439: QString source = mi->first; Chris@439: Chris@439: for (TypeTimeValueMap::const_iterator ttvi = mi->second.begin(); Chris@439: ttvi != mi->second.end(); ++ttvi) { Chris@439: Chris@439: QString type = ttvi->first; Chris@439: Chris@439: // Now we need to work out what sort of model to use for Chris@439: // this source/type combination. Ultimately we'll Chris@439: // hopefully be able to map directly from the type to the Chris@439: // model on the basis of known structures for the types, Chris@439: // but we also want to be able to handle untyped data Chris@439: // according to its apparent structure so let's do that Chris@439: // first. Chris@439: Chris@439: bool sparse = false; Chris@439: int minValueCount = 0, maxValueCount = 0; Chris@439: Chris@439: extractStructure(ttvi->second, sparse, minValueCount, maxValueCount); Chris@439: Chris@439: cerr << "For source \"" << source.toStdString() << "\", type \"" Chris@439: << type.toStdString() << "\" we have sparse = " << sparse Chris@439: << ", min value count = " << minValueCount << ", max = " Chris@439: << maxValueCount << endl; Chris@439: Chris@439: // Model allocations: Chris@439: // Chris@439: // Sparse, no values: SparseOneDimensionalModel Chris@439: // Chris@439: // Sparse, always 1 value: SparseTimeValueModel Chris@439: // Chris@439: // Sparse, > 1 value: No standard model for this. If Chris@439: // there are always 2 values, perhaps hack it into Chris@439: // NoteModel for now? Or always use SparseTimeValueModel Chris@439: // and discard all but the first value. Chris@439: // Chris@439: // Dense, no values: Meaningless; no suitable model Chris@439: // Chris@439: // Dense, > 0 values: EditableDenseThreeDimensionalModel Chris@439: // Chris@439: // These should just be our fallback positions; we want to Chris@439: // be reading semantic data from the RDF in order to pick Chris@439: // the right model directly Chris@439: Chris@439: enum { SODM, STVM, EDTDM } modelType = SODM; Chris@439: Chris@439: if (sparse) { Chris@439: if (maxValueCount == 0) { Chris@439: modelType = SODM; Chris@439: } else if (minValueCount == 1 && maxValueCount == 1) { Chris@439: modelType = STVM; Chris@439: } else { Chris@439: cerr << "WARNING: No suitable model available for sparse data with between " << minValueCount << " and " << maxValueCount << " values" << endl; Chris@439: modelType = STVM; Chris@439: } Chris@439: } else { Chris@439: if (maxValueCount == 0) { Chris@439: cerr << "WARNING: Dense data set with no values is not meaningful, skipping" << endl; Chris@439: continue; Chris@439: } else { Chris@439: modelType = EDTDM; Chris@439: } Chris@439: } Chris@439: Chris@439: //!!! set model name &c Chris@439: Chris@439: if (modelType == SODM) { Chris@439: Chris@439: SparseOneDimensionalModel *model = Chris@439: new SparseOneDimensionalModel(m_sampleRate, 1, false); Chris@439: Chris@439: fillModel(model, ttvi->second); Chris@439: models.push_back(model); Chris@439: Chris@439: } else if (modelType == STVM) { Chris@439: Chris@439: SparseTimeValueModel *model = Chris@439: new SparseTimeValueModel(m_sampleRate, 1, false); Chris@439: Chris@439: fillModel(model, ttvi->second); Chris@439: models.push_back(model); Chris@439: Chris@439: } else { Chris@439: Chris@439: EditableDenseThreeDimensionalModel *model = Chris@439: new EditableDenseThreeDimensionalModel(m_sampleRate, 1, 0, Chris@439: false); Chris@439: Chris@439: fillModel(model, ttvi->second); Chris@439: models.push_back(model); Chris@439: } Chris@439: } Chris@439: } Chris@439: } Chris@439: Chris@439: void Chris@439: RDFImporterImpl::extractStructure(const TimeValueMap &tvm, Chris@439: bool &sparse, Chris@439: int &minValueCount, Chris@439: int &maxValueCount) Chris@439: { Chris@439: // These are floats intentionally rather than RealTime -- Chris@439: // see logic for handling rounding error below Chris@439: float firstTime = 0.f; Chris@439: float timeStep = 0.f; Chris@439: bool haveTimeStep = false; Chris@439: Chris@439: for (TimeValueMap::const_iterator tvi = tvm.begin(); tvi != tvm.end(); ++tvi) { Chris@439: Chris@439: RealTime time = tvi->first; Chris@439: int valueCount = tvi->second.size(); Chris@439: Chris@439: if (tvi == tvm.begin()) { Chris@439: Chris@439: minValueCount = valueCount; Chris@439: maxValueCount = valueCount; Chris@439: Chris@439: firstTime = time.toDouble(); Chris@439: Chris@439: } else { Chris@439: Chris@439: if (valueCount < minValueCount) minValueCount = valueCount; Chris@439: if (valueCount > maxValueCount) maxValueCount = valueCount; Chris@439: Chris@439: if (!haveTimeStep) { Chris@439: timeStep = time.toDouble() - firstTime; Chris@439: if (timeStep == 0.f) sparse = true; Chris@439: haveTimeStep = true; Chris@439: } else if (!sparse) { Chris@439: // test whether this time is within Chris@439: // rounding-error range of being an integer Chris@439: // multiple of some constant away from the Chris@439: // first time Chris@439: float timeAsFloat = time.toDouble(); Chris@439: int count = int((timeAsFloat - firstTime) / timeStep + 0.5); Chris@439: float expected = firstTime + (timeStep * count); Chris@439: if (fabsf(expected - timeAsFloat) > 1e-6) { Chris@439: cerr << "Event at " << timeAsFloat << " is not evenly spaced -- would expect it to be " << expected << " for a spacing of " << count << " * " << timeStep << endl; Chris@439: sparse = true; Chris@439: } Chris@439: } Chris@439: } Chris@439: } Chris@439: } Chris@439: Chris@439: void Chris@439: RDFImporterImpl::fillModel(SparseOneDimensionalModel *model, Chris@439: const TimeValueMap &tvm) Chris@439: { Chris@439: //!!! labels &c not yet handled Chris@439: Chris@439: for (TimeValueMap::const_iterator tvi = tvm.begin(); Chris@439: tvi != tvm.end(); ++tvi) { Chris@439: Chris@439: RealTime time = tvi->first; Chris@439: long frame = RealTime::realTime2Frame(time, m_sampleRate); Chris@439: Chris@439: SparseOneDimensionalModel::Point point(frame); Chris@439: Chris@439: model->addPoint(point); Chris@439: } Chris@439: } Chris@439: Chris@439: void Chris@439: RDFImporterImpl::fillModel(SparseTimeValueModel *model, Chris@439: const TimeValueMap &tvm) Chris@439: { Chris@439: //!!! labels &c not yet handled Chris@439: Chris@439: for (TimeValueMap::const_iterator tvi = tvm.begin(); Chris@439: tvi != tvm.end(); ++tvi) { Chris@439: Chris@439: RealTime time = tvi->first; Chris@439: long frame = RealTime::realTime2Frame(time, m_sampleRate); Chris@439: Chris@439: float value = 0.f; Chris@439: if (!tvi->second.empty()) value = *tvi->second.begin(); Chris@439: Chris@439: SparseTimeValueModel::Point point(frame, value, ""); Chris@439: Chris@439: model->addPoint(point); Chris@439: } Chris@439: } Chris@439: Chris@439: void Chris@439: RDFImporterImpl::fillModel(EditableDenseThreeDimensionalModel *model, Chris@439: const TimeValueMap &tvm) Chris@439: { Chris@439: //!!! labels &c not yet handled Chris@439: Chris@439: //!!! start time offset not yet handled Chris@439: Chris@439: size_t col = 0; Chris@439: Chris@439: for (TimeValueMap::const_iterator tvi = tvm.begin(); Chris@439: tvi != tvm.end(); ++tvi) { Chris@439: Chris@439: model->setColumn(col++, tvi->second); Chris@439: } Chris@439: } Chris@439: