• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Stellarium / stellarium / 15291801018

28 May 2025 04:52AM UTC coverage: 11.931% (-0.02%) from 11.951%
15291801018

push

github

alex-w
Added new set of navigational stars (XIX century)

0 of 6 new or added lines in 2 files covered. (0.0%)

14124 existing lines in 74 files now uncovered.

14635 of 122664 relevant lines covered (11.93%)

18291.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/core/modules/CustomObjectMgr.cpp
1
/*
2
 * Copyright (C) 2012 Alexander Wolf
3
 *
4
 * This program is free software; you can redistribute it and/or
5
 * modify it under the terms of the GNU General Public License
6
 * as published by the Free Software Foundation; either version 2
7
 * of the License, or (at your option) any later version.
8
 *
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program; if not, write to the Free Software
16
 * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA  02110-1335, USA.
17
 */
18

19
#include "StelPainter.hpp"
20
#include "StelApp.hpp"
21
#include "StelCore.hpp"
22
#include "StelModuleMgr.hpp"
23
#include "StelObjectMgr.hpp"
24
#include "StelTextureMgr.hpp"
25
#include "StelFileMgr.hpp"
26
#include "StelUtils.hpp"
27
#include "StelTranslator.hpp"
28
#include "CustomObjectMgr.hpp"
29
#include "StelJsonParser.hpp"
30

31
#include <QDir>
32
#include <QSettings>
33
#include <QKeyEvent>
34
#include <QFileInfo>
35

36
CustomObjectMgr::CustomObjectMgr()
×
37
        : countMarkers(0)
×
38
        , radiusLimit(15)
×
39
{
40
        setObjectName("CustomObjectMgr");
×
41
        conf = StelApp::getInstance().getSettings();
×
42
        echoToLogfile=conf->value("devel/markers_to_logfile", false).toBool();
×
43
        setFontSize(StelApp::getInstance().getScreenFontSize());
×
44
        connect(&StelApp::getInstance(), SIGNAL(screenFontSizeChanged(int)), this, SLOT(setFontSize(int)));
×
UNCOV
45
}
×
46

UNCOV
47
CustomObjectMgr::~CustomObjectMgr()
×
48
{
49
        StelApp::getInstance().getStelObjectMgr().unSelect();
×
UNCOV
50
}
×
51

UNCOV
52
double CustomObjectMgr::getCallOrder(StelModuleActionName actionName) const
×
53
{
54
        if (actionName==StelModule::ActionDraw)
×
55
                return StelApp::getInstance().getModuleMgr().getModule("LandscapeMgr")->getCallOrder(actionName)+10.;
×
56
        if (actionName==StelModule::ActionHandleMouseClicks)
×
57
                return -11;        
×
UNCOV
58
        return 0;
×
59
}
60

UNCOV
61
void CustomObjectMgr::handleMouseClicks(class QMouseEvent* e)
×
62
{
63
        StelCore *core = StelApp::getInstance().getCore();
×
UNCOV
64
        Vec3d mousePosition = core->getMouseJ2000Pos();
×
65
        // Make sure the modifiers are exact, not just "Shift is pressed", but "Shift is pressed while Ctrl and Alt aren't"
66
        const auto modifiers = e->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier | Qt::AltModifier);
×
67
        const bool modifierIsShift = modifiers == Qt::ShiftModifier;
×
UNCOV
68
        const bool modifierIsShiftAlt = modifiers == (Qt::ShiftModifier | Qt::AltModifier);
×
69
        // Shift + LeftClick -- Add custom marker
UNCOV
70
        if (modifierIsShift && e->button()==Qt::LeftButton && e->type()==QEvent::MouseButtonPress)
×
71
        {
UNCOV
72
                addCustomObject(QString("%1 %2").arg(N_("Marker")).arg(countMarkers + 1), mousePosition, true);
×
73
                if (echoToLogfile)
×
74
                {
75
                        // This is a somewhat hacky way to echo th marker positions to coordinate text.
76
                        // Usable to create coordinate lists e.g. for dark constellations (starless coordinate polygons).
77
                        double ra, dec;
UNCOV
78
                        StelUtils::rectToSphe(&ra, &dec, mousePosition);
×
UNCOV
79
                        qInfo() << QString("%1: [%2, %3]").arg(QString::number(countMarkers), // this has already be increased!
×
80
                                                               QString::number(StelUtils::fmodpos(ra*M_180_PI/15., 24.), 'g', 7),
×
81
                                                               QString::number(dec*M_180_PI, 'g', 7));
×
82
                }
83

UNCOV
84
                e->setAccepted(true);
×
85
                return;
×
86
        }
87
        // Shift + Alt + RightClick -- Removes all custom markers
88
        if(modifierIsShiftAlt && e->button() == Qt::RightButton && e->type() == QEvent::MouseButtonPress)
×
89
        {
90
                //Delete ALL custom markers
91
                removeCustomObjects();
×
UNCOV
92
                e->setAccepted(true);
×
93
                return;
×
94
        }
95
        // Shift + RightClick -- Removes the closest marker within a radius specified within
96
        if (modifierIsShift && e->button()==Qt::RightButton && e->type()==QEvent::MouseButtonPress)
×
97
        {
UNCOV
98
                const StelProjectorP prj = core->getProjection(StelCore::FrameJ2000, StelCore::RefractionAuto);
×
99
                Vec3d winpos;
×
100
                prj->project(mousePosition, winpos);
×
UNCOV
101
                double xpos = winpos[0];
×
102
                double ypos = winpos[1];
×
103

104
                CustomObjectP closest;
×
105
                //Smallest valid radius will be at most `radiusLimit`, so radiusLimit + 10 is plenty as the default
UNCOV
106
                float smallestRad = radiusLimit + 10;
×
107
                for (const auto& cObj : std::as_const(customObjects))
×
108
                {
109
                        //Get the position of the custom object
UNCOV
110
                        Vec3d a = cObj->getJ2000EquatorialPos(core);
×
UNCOV
111
                        prj->project(a, winpos);
×
112
                        //Distance formula to determine how close we clicked to each of the custom objects
UNCOV
113
                        float dist = static_cast<float>(std::sqrt(((xpos-winpos[0])*(xpos-winpos[0])) + ((ypos-winpos[1])*(ypos-winpos[1]))));
×
114
                        //If the position of the object is within our click radius
115
                        if(dist <= radiusLimit && dist < smallestRad)
×
116
                        {
117
                                //Update the closest object and the smallest distance.
UNCOV
118
                                closest = cObj;
×
119
                                smallestRad = dist;
×
120
                        }
121
                }
122
                //If there was a custom object within `radiusLimit` pixels...
123
                if(smallestRad <= radiusLimit)
×
124
                {
125
                        //Remove it and return
UNCOV
126
                        removeCustomObject(closest);
×
127
                        e->setAccepted(true);
×
128
                        return;
×
129
                }
130
        }
×
131
        e->setAccepted(false);
×
132
}
133

134
void CustomObjectMgr::init()
×
135
{
UNCOV
136
        texPointer = StelApp::getInstance().getTextureManager().createTexture(StelFileMgr::getInstallationDir()+"/textures/pointeur2.png");
×
137

138
        customObjects.clear();
×
UNCOV
139
        persistentObjects.clear();
×
140

UNCOV
141
        setMarkersColor(Vec3f(conf->value("color/custom_marker_color", "0.1,1.0,0.1").toString()));
×
142
        setMarkersSize(conf->value("gui/custom_marker_size", 5.f).toFloat());
×
143
        // Limit the click radius to 15px in any direction
UNCOV
144
        setActiveRadiusLimit(conf->value("gui/custom_marker_radius_limit", 15).toInt());
×
UNCOV
145
        setSelectPriority(conf->value("gui/custom_marker_priority", -2.f).toFloat());
×
146

147
        // Custom objects for Search Tool
148
        persistentCOFile = StelFileMgr::getUserDir()+"/data/persistentCustomObjects.json";
×
UNCOV
149
        if(QFileInfo::exists(persistentCOFile))
×
150
        {
151
                //qWarning().noquote() << "CustomObjectMgr: loading file:" << QDir::toNativeSeparators(persistentCOFile);
152
                // Loading list of saved custom objects
UNCOV
153
                loadPersistentObjects();
×
154
        }
155
        else
156
        {
157
                //qWarning().noquote() << "CustomObjectMgr: persistentCustomObjects.json does not exist - creating an empty file:" << QDir::toNativeSeparators(persistentCOFile);
158
                // Create a file with empty list of custom objects
UNCOV
159
                savePersistentObjects();
×
160
        }
161

UNCOV
162
        GETSTELMODULE(StelObjectMgr)->registerStelObjectMgr(this);
×
163
}
×
164

UNCOV
165
void CustomObjectMgr::loadPersistentObjects()
×
166
{
167
        QFile dataFile;
×
168
        dataFile.setFileName(persistentCOFile);
×
169
        if (dataFile.open(QIODevice::ReadOnly | QIODevice::Text))
×
170
        {
171
                QVariantMap map = StelJsonParser::parse(dataFile.readAll()).toMap();
×
UNCOV
172
                dataFile.close();
×
173

174
                QVariantMap pcoMap = map.value("customObjects").toMap();
×
UNCOV
175
                for (auto it=pcoMap.cbegin(), end=pcoMap.cend(); it!=end; ++it)
×
176
                {
UNCOV
177
                        Vec3d coordinates(it.value().toString());
×
178
                        CustomObjectP custObj(new CustomObject(it.key(), coordinates, false));
×
179
                        if (custObj->initialized)
×
180
                                persistentObjects.append(custObj);
×
UNCOV
181
                }
×
182
        }
×
183
        else
184
                qWarning().noquote() << "CustomObjectMgr: cannot open" << QDir::toNativeSeparators(persistentCOFile);
×
UNCOV
185
}
×
186

UNCOV
187
void CustomObjectMgr::savePersistentObjects()
×
188
{
UNCOV
189
        QFile dataFile;
×
UNCOV
190
        dataFile.setFileName(persistentCOFile);
×
191
        if (dataFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text | QIODevice::Unbuffered))
×
192
        {
193
                StelCore* core = StelApp::getInstance().getCore();
×
194
                QVariantMap map, pcObjects;
×
195
                for (const auto& cObj : std::as_const(persistentObjects))
×
196
                {
UNCOV
197
                        if (cObj && cObj->initialized)
×
198
                        {
199
                                pcObjects[cObj->getID()] = cObj->getJ2000EquatorialPos(core).toStr();
×
200
                        }
201
                }
UNCOV
202
                map["customObjects"] = pcObjects;
×
203

204
                StelJsonParser::write(map, &dataFile);
×
205
                dataFile.flush();
×
206
                dataFile.close();
×
UNCOV
207
        }
×
208
        else
UNCOV
209
                qDebug().noquote() << "CustomObjectMgr: can't create a file:" << QDir::toNativeSeparators(persistentCOFile);
×
210
}
×
211

UNCOV
212
void CustomObjectMgr::deinit()
×
213
{
UNCOV
214
        customObjects.clear();
×
215
        persistentObjects.clear();
×
UNCOV
216
        texPointer.clear();
×
UNCOV
217
}
×
218

UNCOV
219
void CustomObjectMgr::setSelectPriority(float priority)
×
220
{
221
        CustomObject::selectPriority = priority;
×
222
}
×
223

224
float CustomObjectMgr::getSelectPriority() const
×
225
{
226
        return CustomObject::selectPriority;
×
227
}
228

UNCOV
229
void CustomObjectMgr::removePersistentObjects()
×
230
{
231
        GETSTELMODULE(StelObjectMgr)->unSelect();
×
232
        persistentObjects.clear();
×
UNCOV
233
        savePersistentObjects();
×
234
        emit StelApp::getInstance().getCore()->updateSearchLists();
×
235
}
×
236

237
void CustomObjectMgr::addPersistentObject(const QString& designation, Vec3d coordinates)
×
238
{
UNCOV
239
        if (!designation.isEmpty())
×
240
        {
UNCOV
241
                CustomObjectP custObj(new CustomObject(designation, coordinates, false));
×
242
                if (custObj->initialized)
×
UNCOV
243
                        persistentObjects.append(custObj);
×
244

245
                savePersistentObjects();
×
246
                emit StelApp::getInstance().getCore()->updateSearchLists();
×
UNCOV
247
        }
×
248
}
×
249

250

251
void CustomObjectMgr::addCustomObject(const QString& designation, Vec3d coordinates, bool isVisible)
×
252
{
253
        if (!designation.isEmpty())
×
254
        {
255
                CustomObjectP custObj(new CustomObject(designation, coordinates, isVisible));
×
UNCOV
256
                if (custObj->initialized)
×
257
                        customObjects.append(custObj);
×
258

259
                if (isVisible)
×
260
                        countMarkers++;
×
261

262
                emit StelApp::getInstance().getCore()->updateSearchLists();
×
263
        }
×
UNCOV
264
}
×
265

UNCOV
266
void CustomObjectMgr::addCustomObject(const QString& designation, const QString &ra, const QString &dec, bool isVisible)
×
267
{
268
        Vec3d j2000;
×
269
        double dRa = StelUtils::getDecAngle(ra);
×
270
        double dDec = StelUtils::getDecAngle(dec);
×
UNCOV
271
        StelUtils::spheToRect(dRa,dDec,j2000);
×
272

273
        addCustomObject(designation, j2000, isVisible);
×
UNCOV
274
}
×
275

UNCOV
276
void CustomObjectMgr::addCustomObjectRaDec(const QString& designation, const QString &ra, const QString &dec, bool isVisible)
×
277
{
278
        Vec3d aim;
×
279
        double dRa = StelUtils::getDecAngle(ra);
×
UNCOV
280
        double dDec = StelUtils::getDecAngle(dec);
×
281
        StelUtils::spheToRect(dRa, dDec, aim);
×
282

UNCOV
283
        addCustomObject(designation, StelApp::getInstance().getCore()->equinoxEquToJ2000(aim, StelCore::RefractionOff), isVisible);
×
284
}
×
285

286
void CustomObjectMgr::addCustomObjectAltAzi(const QString& designation, const QString &alt, const QString &azi, bool isVisible)
×
287
{
UNCOV
288
        Vec3d aim;
×
289
        double dAlt = StelUtils::getDecAngle(alt);
×
UNCOV
290
        double dAzi = M_PI - StelUtils::getDecAngle(azi);
×
291

292
        if (StelApp::getInstance().getFlagSouthAzimuthUsage())
×
UNCOV
293
                dAzi -= M_PI;
×
294

295
        StelUtils::spheToRect(dAzi, dAlt, aim);
×
296

UNCOV
297
        addCustomObject(designation, StelApp::getInstance().getCore()->altAzToJ2000(aim, StelCore::RefractionAuto), isVisible);
×
298
}
×
299

300
void CustomObjectMgr::removeCustomObjects()
×
301
{
302
        GETSTELMODULE(StelObjectMgr)->unSelect();
×
303
        customObjects.clear();
×
304
        //This marker count can be set to 0 because there will be no markers left and a duplicate will be impossible
305
        countMarkers = 0;
×
UNCOV
306
        emit StelApp::getInstance().getCore()->updateSearchLists();
×
307
}
×
308

UNCOV
309
void CustomObjectMgr::removeCustomObject(CustomObjectP obj)
×
310
{
311
        GETSTELMODULE(StelObjectMgr)->unSelect();
×
312
        customObjects.removeOne(obj);
×
UNCOV
313
        emit StelApp::getInstance().getCore()->updateSearchLists();
×
314
}
×
315

316
void CustomObjectMgr::removeCustomObject(QString englishName)
×
317
{
318
        GETSTELMODULE(StelObjectMgr)->unSelect();
×
319
        for (const auto& cObj : std::as_const(customObjects))
×
320
        {
321
                //If we have a match for the thing we want to delete
322
                if(cObj && cObj->getEnglishName()==englishName && cObj->initialized)
×
UNCOV
323
                        customObjects.removeOne(cObj);
×
324
        }        
325
}
×
326

UNCOV
327
void CustomObjectMgr::draw(StelCore* core)
×
328
{
UNCOV
329
        StelProjectorP prj = core->getProjection(StelCore::FrameJ2000);
×
330
        StelPainter painter(prj);
×
331
        painter.setFont(font);
×
332

UNCOV
333
        for (const auto& cObj : std::as_const(customObjects))
×
334
        {
335
                if (cObj && cObj->initialized)
×
336
                        cObj->draw(core, &painter);
×
337
        }
338

339
        for (const auto& pObj : std::as_const(persistentObjects))
×
340
        {
341
                if (pObj && pObj->initialized)
×
UNCOV
342
                        pObj->draw(core, &painter);
×
343
        }
344

345
        static StelObjectMgr *sObjMgr=GETSTELMODULE(StelObjectMgr);
×
UNCOV
346
        if (sObjMgr->getFlagSelectedObjectPointer())
×
347
                drawPointer(core, painter);
×
348
}
×
349

350
void CustomObjectMgr::drawPointer(StelCore* core, StelPainter& painter)
×
351
{
352
        const StelProjectorP prj = core->getProjection(StelCore::FrameJ2000);
×
353

UNCOV
354
        static StelObjectMgr *sObjMgr=GETSTELMODULE(StelObjectMgr);
×
355
        const QList<StelObjectP> newSelected = sObjMgr->getSelectedObject("CustomObject");
×
356
        if (!newSelected.empty())
×
357
        {
358
                const StelObjectP obj = newSelected[0];
×
359
                Vec3f pos=obj->getJ2000EquatorialPos(core).toVec3f();
×
360

UNCOV
361
                Vec3f screenpos;
×
362
                // Compute 2D pos and return if outside screen
UNCOV
363
                if (!painter.getProjector()->project(pos, screenpos))
×
364
                        return;
×
365

366
                painter.setColor(obj->getInfoColor());
×
367
                texPointer->bind();
×
368
                painter.setBlending(true);
×
369
                painter.drawSprite2dMode(screenpos[0], screenpos[1], 13.f, static_cast<float>(StelApp::getInstance().getTotalRunTime()*40.));
×
UNCOV
370
        }
×
371
}
×
372

373
QList<StelObjectP> CustomObjectMgr::searchAround(const Vec3d& av, double limitFov, const StelCore* core) const
×
374
{
375
        QList<StelObjectP> result;
×
376

377
        Vec3d v(av);
×
UNCOV
378
        v.normalize();
×
379
        const double cosLimFov = cos(limitFov * M_PI/180.);
×
UNCOV
380
        Vec3d equPos;
×
381

UNCOV
382
        for (const auto& cObj : customObjects)
×
383
        {
384
                if (cObj->initialized)
×
385
                {
386
                        equPos = cObj->getJ2000EquatorialPos(core);
×
UNCOV
387
                        equPos.normalize();
×
388
                        if (equPos.dot(v) >= cosLimFov)
×
389
                        {
390
                                result.append(qSharedPointerCast<StelObject>(cObj));
×
391
                        }
392
                }
393
        }
394

UNCOV
395
        for (const auto& pObj : persistentObjects)
×
396
        {
397
                if (pObj->initialized)
×
398
                {
UNCOV
399
                        equPos = pObj->getJ2000EquatorialPos(core);
×
400
                        equPos.normalize();
×
UNCOV
401
                        if (equPos.dot(v) >= cosLimFov)
×
402
                        {
UNCOV
403
                                result.append(qSharedPointerCast<StelObject>(pObj));
×
404
                        }
405
                }
406
        }
407

408
        return result;
×
UNCOV
409
}
×
410

411
StelObjectP CustomObjectMgr::searchByName(const QString& englishName) const
×
412
{
UNCOV
413
        for (const auto& cObj : customObjects)
×
414
        {
UNCOV
415
                if (cObj->getEnglishName().toUpper() == englishName.toUpper())
×
UNCOV
416
                        return qSharedPointerCast<StelObject>(cObj);
×
417
        }
418

419
        for (const auto& pObj : persistentObjects)
×
420
        {
421
                if (pObj->getEnglishName().toUpper() == englishName.toUpper())
×
422
                        return qSharedPointerCast<StelObject>(pObj);
×
423
        }
424

425
        return Q_NULLPTR;
×
426
}
427

428
StelObjectP CustomObjectMgr::searchByNameI18n(const QString& nameI18n) const
×
429
{
UNCOV
430
        for (const auto& cObj : customObjects)
×
431
        {
UNCOV
432
                if (cObj->getNameI18n().toUpper() == nameI18n.toUpper())
×
UNCOV
433
                        return qSharedPointerCast<StelObject>(cObj);
×
434
        }
435

436
        for (const auto& pObj : persistentObjects)
×
437
        {
438
                if (pObj->getNameI18n().toUpper() == nameI18n.toUpper())
×
UNCOV
439
                        return qSharedPointerCast<StelObject>(pObj);
×
440
        }
441

442
        return Q_NULLPTR;
×
443
}
444

UNCOV
445
QStringList CustomObjectMgr::listAllObjects(bool inEnglish) const
×
446
{
UNCOV
447
        QStringList result;
×
448

UNCOV
449
        if (inEnglish)
×
450
        {
451
                for (const auto& cObj : customObjects)
×
452
                {
453
                        result << cObj->getEnglishName();
×
454
                }
455
                for (const auto& pObj : persistentObjects)
×
456
                {
457
                        result << pObj->getEnglishName();
×
458
                }
459
        }
460
        else
461
        {
UNCOV
462
                for (const auto& cObj : customObjects)
×
463
                {
UNCOV
464
                        result << cObj->getNameI18n();
×
465
                }
466
                for (const auto& pObj : persistentObjects)
×
467
                {
468
                        result << pObj->getNameI18n();
×
469
                }
470
        }
471
        return result;
×
472
}
×
473

UNCOV
474
void CustomObjectMgr::selectedObjectChange(StelModule::StelModuleSelectAction)
×
475
{
UNCOV
476
        const QList<StelObjectP> newSelected = GETSTELMODULE(StelObjectMgr)->getSelectedObject("CustomObject");
×
477
        if (!newSelected.empty())
×
478
        {
UNCOV
479
                setSelected(qSharedPointerCast<CustomObject>(newSelected[0]));
×
480
        }
481
        else
482
                GETSTELMODULE(StelObjectMgr)->unSelect();
×
483
}
×
484

485
// Set selected planets by englishName
486
void CustomObjectMgr::setSelected(const QString& englishName)
×
487
{
488
        setSelected(searchByEnglishName(englishName));
×
UNCOV
489
}
×
490

UNCOV
491
void CustomObjectMgr::setSelected(CustomObjectP obj)
×
492
{
493
        if (obj && obj->getType() == "CustomObject")
×
UNCOV
494
                selected = obj;
×
495
        else
UNCOV
496
                selected.clear();
×
497
}
×
498

UNCOV
499
CustomObjectP CustomObjectMgr::searchByEnglishName(QString customObjectEnglishName) const
×
500
{
UNCOV
501
        for (const auto& p : customObjects)
×
502
        {
UNCOV
503
                if (p->getEnglishName() == customObjectEnglishName)
×
504
                        return p;
×
505
        }
506
        for (const auto& po : persistentObjects)
×
507
        {
UNCOV
508
                if (po->getEnglishName() == customObjectEnglishName)
×
509
                        return po;
×
510
        }
511
        return CustomObjectP();
×
512
}
513

514
// Set/Get planets names color
UNCOV
515
void CustomObjectMgr::setMarkersColor(const Vec3f& c)
×
516
{
517
        CustomObject::markerColor = c;
×
UNCOV
518
}
×
519

UNCOV
520
Vec3f CustomObjectMgr::getMarkersColor(void) const
×
521
{
UNCOV
522
        return CustomObject::markerColor;
×
523
}
524

UNCOV
525
void CustomObjectMgr::setMarkersSize(const float size)
×
526
{
527
        CustomObject::markerSize = size;
×
UNCOV
528
}
×
529

UNCOV
530
float CustomObjectMgr::getMarkersSize() const
×
531
{
UNCOV
532
        return CustomObject::markerSize;
×
533
}
534

UNCOV
535
void CustomObjectMgr::setActiveRadiusLimit(const int radius)
×
536
{
UNCOV
537
        radiusLimit = radius;
×
UNCOV
538
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc