Skip to content

Commit

Permalink
First public release
Browse files Browse the repository at this point in the history
  • Loading branch information
alextutubalin committed Mar 28, 2019
1 parent 199f95a commit 2c3cbf6
Show file tree
Hide file tree
Showing 22 changed files with 700 additions and 2 deletions.
7 changes: 7 additions & 0 deletions DOFCalc.pro
@@ -0,0 +1,7 @@
QT += quick
CONFIG += c++11
DEFINES += QT_DEPRECATED_WARNINGS
SOURCES += cpp/main.cpp cpp/datamodels.cpp cpp/dofcalcobject.cpp
RESOURCES += qml.qrc
HEADERS += cpp/datamodels.h cpp/dofcalcobject.h cpp/dofstrings.h

34 changes: 32 additions & 2 deletions README.md
@@ -1,2 +1,32 @@
# DOFCalc
Depth of Field Calculator for mobile
# DOFCalc v0.1
## README

This is my first application for mobile phones, and my first on Qt+QML.

When I started studying QML, I decided that it might be useful to write a small but complete program that I will use myself.
Aside from this, the DOF calculators that I found for mobile platforms seemed very overloaded: when I'm out shooting, I need to quickly input three parameters (focal length, aperture, distance) and get the result (hyperfocal distance, near and far limits of acceptable sharpness). I don't want to see anything else; this is where the idea for the program was born.

It has no (common in the modern day) bells and whistles, such as
* setting the circle of confusion by choosing from a million options ("Nikon, APS-C", "Canon APS-H"...), the circle is set directly, in microns
* teleconverter support (meaning automatic re-calculation of lens speed and focal length).

## Notes
When I had a working prototype ready, I noticed that there did not turn out to be too much code, and decided, out of sporting interest, to try to use as little code as possible with minimal losses in readability.
For this reason:
* the braces are set differently than I usually do, but that minimizes the number of lines,
* any repetition was mercilessly generalized, there are almost no repeated pieces of code,
* data models are designed based on ideas of compactness, not readability,
With these tricks I was able to keep entire project below 600 lines (before copyright statements were added), this is good enough for some really useful project.

## TODO
* I'm not happy with automated font size selection code, it works under Android (any reasonable device screen size), but not on Windows. It should be some more elegant way to set default font size in QML (any advices are welcome).
* Build/deploy files are not included. Only code and minimal qmake .pro file.

The project is not done yet: there are other things a photographer (more precisely, I myself) needs in the field; so the work is to be quietly continued.

## Licensing
DOFCalc is (C) Copyright 2019 by Alex Tutubalin, LibRaw LLC, lexa@libraw.org. All Right Reserved

DOFCalc is distributed under the terms of GNU General Public License v3

Alex Tutubalin, LibRaw LLC, Mar 28, 2019
76 changes: 76 additions & 0 deletions cpp/datamodels.cpp
@@ -0,0 +1,76 @@
/* DOFCalc v0.1
(C)opyright 2019 Alex Tutubalin, LibRaw LLC, lexa@libraw.org
The program is licensed under the terms of GNU General Public License v3 */

#include "datamodels.h"
#include <QSet>
#include <QRegularExpression>

QVariant focalModel::data(const QModelIndex &index, int role /* = Qt::DisplayRole */) const {
int row = index.row();
if (row < 0 || row >= rowCount()) return QVariant();
switch (role) {
case DOFDisplayRole: if (!datalist[row].big) return S_longdash; // fall-through
case DOFPrintRole: return formatString.arg(datalist[row].value, 0, 'f', digits(datalist[row].value));
case DOFValueRole: return datalist[row].value;
case DOFSizeRole: return int(datalist[row].big);
}
return QVariant();
}

void focalModel::populate0(const QHash<QString, int>&) {
struct focal_params_t { qreal begin, step; int div; };
QList<focal_params_t> focal_params(QList<focal_params_t>{ {5, 1, 5}, { 30,5,10, }, { 150,10,50 }, { 250,25,50 }, { 400,50,100 }, { 1000,100,200 }, { 1600,0,100 } });
for (int i = 0; i < focal_params.size(); i++) {
datalist.push_back(floatitem_t(focal_params[i].begin, true));
if (focal_params[i].step < 0.001) break;
qreal start = focal_params[i].begin + focal_params[i].step;
while (start < focal_params[i + 1].begin) {
datalist.push_back(floatitem_t(start, !(int(start) % focal_params[i].div)));
start += focal_params[i].step;
}
}
}

void apertureModel::populate0(const QHash<QString, int>& params) {
QList<qreal>fstoplist[4] = { // log2 based calculations do not match widely used aperture scales; 1/2 and 1/3 scales are copied from Seconic L558 scale; 1/4 scale matches seconic 1/2
{QList<qreal>{0.7, 1, 1.4, 2, 2.8, 4, 5.6, 8, 11, 16, 22, 32, 45, 64, 90, 128, 180, 256}},
{QList<qreal>{0.7, 0.8, 1, 1.2,1.4,1.7, 2,2.4, 2.8, 3.4, 4, 4.8, 5.6, 6.7, 8, 9.5, 11,13, 16,19, 22,27, 32,38, 45,54, 64, 76, 90, 108, 128, 152, 180, 215, 256}},
{QList<qreal>{0.7, 0.8, 0.9, 1, 1.1,1.3, 1.4, 1.6, 1.8, 2, 2.2, 2.5, 2.8, 3.2, 3.6, 4, 4.5, 5, 5.6, 6.3, 7.1, 8, 9, 10, 11, 13,14, 16, 18, 20, 22, 25, 29, 32,
36, 40, 45, 51, 57, 64,72, 81, 90, 102,114, 128,144, 161, 180,200,225,256 }},
{QList<qreal>{0.7,0.75, 0.8,0.9, 1, 1.1, 1.2, 1.3,1.4, 1.5, 1.7,1.8, 2,2.2, 2.4,2.6, 2.8, 3.1, 3.4, 3.7, 4,4.4,4.8, 5.2, 5.6,6.2, 6.7,7.3, 8,8.7, 9.5,10, 11,12,13,
15,16,17,19,21,22,25,27,29,32,34,38,42,45,50,54,58,64,68,76,84,90,100,108,116,128, 135, 152, 165, 180, 200, 215, 230, 256}},
};

int ns = qBound(0, params[s_aperture], 3);
for (int i = 0; i < fstoplist[ns].size(); i++)
datalist.push_back(floatitem_t(fstoplist[ns][i], !(i % (ns + 1))));
}

QString cutzeroes(qreal v, int digits) {
QString s = QString("%1").arg(v, 0, 'f', digits);
if (s.contains('.'))
s.replace(QRegularExpression("\\.0+$"), "");
if (s.contains('.'))
s.replace(QRegularExpression("0+$"), "");
return s;
}

QVariant distanceModel::data(const QModelIndex &index, int role /* = Qt::DisplayRole */) const {
if (index.row() >= 0 && index.row() < rowCount() && role == DOFDisplayRole && datalist[index.row()].big)
return formatString.arg(cutzeroes(datalist[index.row()].value, digits(datalist[index.row()].value)));
return focalModel::data(index, role);
}

void distanceModel::populate0(const QHash<QString, int>& settings) {
QList<qreal> l10 = QList<qreal>{ 0.1, 0.125, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5, 0.6, 0.75, 1 }; // Again: manual distance list is much better than generated one
QSet<int> B = QSet<int>{ 0, 3, 5, 7 };
int f1 = qBound(1, settings[s_distance] + 1, 3);
for (int i = 0; i < 4; i++)
for (int j = 0; j < l10.size() - 1; j++) {
datalist.push_back(floatitem_t(std::pow(10.0, qreal(i))*l10[j], f1 > 1 || B.contains(j)));
for (int k = 1; k < f1; k++)
datalist.push_back(floatitem_t(l10[j] * std::exp(std::log(l10[j + 1] / l10[j]) / qreal(f1)*k)*std::pow(10.0, qreal(i)), false));
}
datalist.push_back(floatitem_t(1000, true));
}
58 changes: 58 additions & 0 deletions cpp/datamodels.h
@@ -0,0 +1,58 @@
/* DOFCalc v0.1
(C)opyright 2019 Alex Tutubalin, LibRaw LLC, lexa@libraw.org
The program is licensed under the terms of GNU General Public License v3 */

#pragma once
#include <QAbstractListModel>
#include "dofstrings.h"
#include <cmath>

enum DOFRoles { DOFDisplayRole = Qt::UserRole + 1, DOFValueRole, DOFSizeRole, DOFPrintRole };

struct floatitem_t {
qreal value;
bool big;
floatitem_t() : value(1), big(false) {}
floatitem_t(qreal v, bool b) : value(v), big(b) {}
floatitem_t(const floatitem_t& s) : value(s.value), big(s.big) {}
};

class focalModel : public QAbstractListModel {
Q_OBJECT
public:
focalModel(QObject *parent = nullptr) : QAbstractListModel(parent), formatString(f_focal) { }
int rowCount(const QModelIndex &parent = QModelIndex()) const { Q_UNUSED(parent); return datalist.size(); }
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
QHash<int, QByteArray> roleNames() const {
return QHash<int, QByteArray>{ {DOFDisplayRole, "displayname"}, { DOFValueRole, "value" }, { DOFSizeRole, "rowsize" }};
}
void populate(const QHash<QString, int>& s) { beginResetModel(); datalist.clear(); populate0(s); endResetModel(); }
protected:
virtual void populate0(const QHash<QString, int>& s);
virtual int digits(qreal) const { return 0; }
QString formatString;
QList<floatitem_t> datalist;
};

class apertureModel : public focalModel {
Q_OBJECT
public:
apertureModel(QObject *parent = nullptr) : focalModel(parent) { formatString = QString(f_aperture); }
protected:
virtual void populate0(const QHash<QString, int>&);
virtual int digits(qreal val) const {
if (val > 12) return 0;
else if (val >= 1) return (std::abs(val - std::round(val)) < 0.1 ? 0 : 1);
else return (std::abs(val * 10 - std::round(val * 10)) < 0.5 ? 1 : 2);
}
};

class distanceModel : public focalModel {
Q_OBJECT
public:
distanceModel(QObject *parent = nullptr) : focalModel(parent) { formatString = QString(f_distance); }
virtual void populate0(const QHash<QString, int>&);
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
protected:
virtual int digits(qreal val) const { return int(qMax(0.0, std::round(2.0 - std::log10(val)))); }
};
167 changes: 167 additions & 0 deletions cpp/dofcalcobject.cpp
@@ -0,0 +1,167 @@
/* DOFCalc v0.1
(C)opyright 2019 Alex Tutubalin, LibRaw LLC, lexa@libraw.org
The program is licensed under the terms of GNU General Public License v3 */

#include "dofcalcobject.h"
#include <QGuiApplication>
#include <QQmlContext>
#include <QWindow>
#include <QScreen>

DOFCalculator::DOFCalculator(QQmlApplicationEngine *e, QObject* parent /* = 0 */) : QObject(parent), engine(e) {
QSize screensz = QGuiApplication::screens().at(0)->availableSize();
int fs = QSysInfo::productType() == QString("windows") ? 18 : qBound(8, qMin(screensz.width(), screensz.height()) / 32, 30);
settings.loadSettings(fs);
params.loadSettings();
setFontAndHeading(settings.values[s_fontSize]);

datamodels[mv_focal] = new focalModel();
datamodels[mv_aperture] = new apertureModel();
datamodels[mv_distance] = new distanceModel();

foreach(const QString& mname, datamodels.keys())
{
datamodels[mname]->populate(settings.values);
engine->rootContext()->setContextProperty(QString(t_Name2Model).arg(mname), datamodels[mname]);
}
}

void DOFCalculator::setViewIndexAndShow(const QString &mname, const QString& object, QObject* flist) {
int index = findNearestIndex(datamodels[mname], params.params[mname]);
if (index >= 0) {
savedVelocity[object] = flist->property(p_highlightMoveVelocity).toInt();
flist->setProperty(p_highlightMoveVelocity, 99999);
flist->setProperty(p_currentIndex, index);
setText(QString(t_Name2DisplayField).arg(mname), datamodels[mname]->index(index).data(DOFPrintRole).toString());
}
}

void DOFCalculator::setFontAndHeading(int fontsize) {
engine->rootContext()->setContextProperty("globalFontSize", settings.values[s_fontSize]);
if (QWindow *w = (QGuiApplication::topLevelWindows().size() > 0 ? QGuiApplication::topLevelWindows().at(0) : nullptr)) {
int bigfont = (qMin(w->geometry().width(), w->geometry().height()) / qMax(1, fontsize) < 28);
setText("hyperfocalPrefix", bigfont ? "HF:" : "HFocal:");
setText("dofFarPrefix", bigfont ? "F:" : "Far:");
setText("dofNearPrefix", bigfont ? "N:" : "Near:");
}
}

void DOFCalculator::afterQMLStart(QGuiApplication* app) {
connect(app, &QGuiApplication::applicationStateChanged, this, &DOFCalculator::appStateChanged);

setFontAndHeading(settings.values[s_fontSize]);
if (QObject *dialog = findObjectByName("settingsDialog"))
connect(dialog, SIGNAL(settingsDialogChanged()), this, SLOT(settingsChanged()));

foreach(const QString& mname, datamodels.keys()) {
QString object = QString(t_Name2ListView).arg(mname);
if (QObject *flist = findObjectByName(object)) {
scrollers[mname] = flist;
setViewIndexAndShow(mname, object, flist);
connect(flist, SIGNAL(myViewIndexChanged(int)), this, SLOT(scrollerChanged(int)));
}
}
foreach(const QString& key, settings.values.keys()) {
QObject *obj = nullptr;
if (key.startsWith(t_IsIndexVar) && (obj = findObjectByName(key.right(t_IndexNameLen))))
obj->setProperty(p_currentIndex, settings.values[key]);
else if ((obj = findObjectByName(key)))
obj->setProperty(p_value, settings.values[key]);
}
calc();
}

int DOFCalculator::repopulate(const QString& mname) {
if (!datamodels[mname] || !scrollers[mname]) return 0;
QString object = QString(t_Name2ListView).arg(mname);
if (QObject *flist = findObjectByName(object)) {
scrollers[mname]->blockSignals(1);
datamodels[mname]->populate(settings.values);
setViewIndexAndShow(mname, object, flist);
scrollers[mname]->blockSignals(0);
return 1;
}
return 0;
}

void DOFCalculator::settingsChanged() {
dofsettings_t osettings = settings;
foreach(const QString& key, settings.values.keys()) {
QObject *obj = nullptr;
if (key.startsWith(t_IsIndexVar) && (obj = findObjectByName(key.right(t_IndexNameLen))))
settings.values[key] = obj->property(p_currentIndex).toInt();
else if ((obj = findObjectByName(key)))
settings.values[key] = obj->property(p_value).toInt();
}
if (osettings.values == settings.values) return;
settings.saveSettings();
int repopulated = 0;
if (settings.values[s_aperture] != osettings.values[s_aperture]) repopulated += repopulate(mv_aperture);
if (settings.values[s_distance] != osettings.values[s_distance]) repopulated += repopulate(mv_distance);
if (repopulated) QTimer::singleShot(2000, this, SLOT(restoreVelocities()));
setFontAndHeading(settings.values[s_fontSize]);
calc();
}

void DOFCalculator::scrollerChanged(int dindex) {
dofparams_t oparams = params;
QString key;
foreach(const QString& mn, scrollers.keys())
if (scrollers[mn] == sender())
key = mn;
if (key.length() && datamodels.contains(key) && dindex >= 0 && dindex < datamodels[key]->rowCount()) {
params.params[key] = datamodels[key]->index(dindex).data(DOFValueRole).toReal();
if (params.params == oparams.params) return;
calc();
setText(QString(t_Name2DisplayField).arg(key), datamodels[key]->index(dindex).data(DOFPrintRole).toString());
}
}

int DOFCalculator::findNearestIndex(QAbstractListModel* model, qreal wanted) {
if (model->rowCount() < 2) return model->rowCount() - 1;
qreal current = model->index(0).data(DOFValueRole).toReal();
qreal diff = qAbs(current - wanted);
for (int i = 1; i < model->rowCount(); i++) {
qreal val = model->index(i).data(DOFValueRole).toReal();
if (diff < qAbs(val - wanted)) return i - 1;
diff = qAbs(val - wanted);
}
return model->rowCount() - 1;
}

QObject* DOFCalculator::findObjectByName(const QString& object) {
foreach(QObject* o, engine->rootObjects())
if (QObject *item = o->findChild<QObject*>(object))
return item;
return nullptr;
}

void DOFCalculator::restoreVelocities() {
foreach(const QString& oname, savedVelocity.keys())
if (QObject * item = findObjectByName(oname))
item->setProperty(p_highlightMoveVelocity, savedVelocity[oname]);
savedVelocity.clear();
}

void DOFCalculator::calc() {
qreal focalM = params.params[mv_focal] / 1000.0, CoCM = qreal(settings.values["coc"]) / 1000.0 / 1000.0, distance = params.params[mv_distance], aperture = params.params[mv_aperture];
qreal airy = 0.38 * aperture / 1000.0 / 1000.0; // 0.38um - about shortest visible wavelength
CoCM = qMax(CoCM, airy);
qreal focalM2 = focalM * focalM;
qreal H = focalM2 / aperture / CoCM + focalM;
setText("hyperfocalString", QString(f_distance).arg(H, 0, 'f', int(qMax(0.0, std::round(2.0 - std::log10(H))))));
if (focalM > distance * 0.9)
setDOF(S_longdash, S_longdash);
else
{
qreal DOFNear = distance * focalM2 / (focalM2 - aperture * focalM * CoCM + aperture * distance * CoCM);
qreal DOFFar = distance * focalM2 / (focalM2 + aperture * focalM * CoCM - aperture * distance * CoCM);

if (DOFFar > 0) {
int chars = qMax(0, int(1 - int(std::log10((DOFFar - DOFNear) / 10))));
setDOF(QString(f_distance).arg(DOFNear, 0, 'f', chars), QString(f_distance).arg(DOFFar, 0, 'f', chars));
}
else
setDOF(QString(f_distance).arg(DOFNear, 0, 'f', qMax(0, 2 - int(std::log10(DOFNear)))), QString(QChar(0x221e)));
}
}

0 comments on commit 2c3cbf6

Please sign in to comment.