Loading...
Searching...
No Matches
FixtureDatabase.hpp
1#pragma once
2#include <Library/LibrarySettings.hpp>
3#include <Protocols/Artnet/ArtnetSpecificSettings.hpp>
4
5#include <score/model/tree/TreeNodeItemModel.hpp>
6#include <score/tools/FindStringInFile.hpp>
7
8#include <ossia/detail/flat_map.hpp>
9#include <ossia/detail/hash_map.hpp>
10#include <ossia/detail/math.hpp>
11#include <ossia/detail/string_algorithms.hpp>
12
13#include <QDirIterator>
14
15#include <re2/re2.h>
16
17#include <ctre.hpp>
18namespace Protocols
19{
20
21static constexpr auto leq_rexp_str = ctll::fixed_string{R"_(<=([0-9]+))_"};
22static constexpr auto geq_rexp_str = ctll::fixed_string{R"_(>=([0-9]+))_"};
23static constexpr auto lst_rexp_str = ctll::fixed_string{R"_(<([0-9]+))_"};
24static constexpr auto gst_rexp_str = ctll::fixed_string{R"_(>([0-9]+))_"};
25static constexpr auto eq_rexp_str = ctll::fixed_string{R"_(=([0-9]+))_"};
26static constexpr auto arith1_rexp_str = ctll::fixed_string{R"_(([0-9]+)n)_"};
27static constexpr auto arith_rexp_str = ctll::fixed_string{R"_(([0-9]+)n\+([0-9]+))_"};
28
29static constexpr auto leq_rex = ctre::match<leq_rexp_str>;
30static constexpr auto geq_rex = ctre::match<geq_rexp_str>;
31static constexpr auto lst_rex = ctre::match<lst_rexp_str>;
32static constexpr auto gst_rex = ctre::match<gst_rexp_str>;
33static constexpr auto eq_rex = ctre::match<eq_rexp_str>;
34static constexpr auto arith1_rex = ctre::match<arith1_rexp_str>;
35static constexpr auto arith_rex = ctre::match<arith_rexp_str>;
36
37static std::vector<QString> fixturesLibraryPaths()
38{
39 auto libPath
40 = score::AppContext().settings<Library::Settings::Model>().getPackagesPath();
41 QDirIterator it{
42 libPath,
43 {"fixtures"},
44 QDir::Dirs,
45 QDirIterator::Subdirectories | QDirIterator::FollowSymlinks};
46
47 std::vector<QString> fixtures;
48 while(it.hasNext())
49 {
50 QDir dirpath = it.next();
51 if(!dirpath.entryList({"manufacturers.json"}, QDir::Filter::Files).isEmpty())
52 {
53 fixtures.push_back(dirpath.absolutePath());
54 }
55 }
56 return fixtures;
57}
58
59static ossia::flat_map<QString, QString>
60readManufacturers(const rapidjson::Document& doc)
61{
62 ossia::flat_map<QString, QString> map;
63 map.reserve(100);
64 if(!doc.IsObject())
65 return map;
66
67 // TODO coroutines
68 for(auto it = doc.MemberBegin(); it != doc.MemberEnd(); ++it)
69 {
70 if(it->value.IsObject())
71 {
72 if(auto name_it = it->value.FindMember("name"); name_it != it->value.MemberEnd())
73 {
74 map.insert(
75 {QString::fromUtf8(it->name.GetString()),
76 QString::fromUtf8(name_it->value.GetString())});
77 }
78 }
79 }
80 return map;
81}
82
83struct Pixel
84{
85 int16_t x{}, y{}, z{};
86 QString name;
87};
88
90{
91 QString name;
92 std::vector<int> pixels; // Indices in pixels array of PixelMatrix
93};
94
96{
97 std::vector<Pixel> pixels;
98 std::vector<PixelGroup> groups;
99};
100
102{
103 QString name;
104 std::vector<QString> allChannels;
105 std::vector<Artnet::Channel> channels;
106
107 QString content() const noexcept
108 {
109 QString str;
110 str.reserve(500);
111 int k = 0;
112 for(auto& chan : allChannels)
113 {
114 str += QString::number(k + 1);
115 str += ": \t";
116 str += chan.isEmpty() ? "<No function>" : chan;
117 str += "\n";
118 k++;
119 }
120 return str;
121 }
122};
123
125{
126 QString type;
127 QString name;
128};
129
130struct Wheel
131{
132 QString name;
133 std::vector<WheelSlot> wheelslots;
134};
136{
137public:
138 using ChannelMap = ossia::hash_map<QString, Artnet::Channel>;
139 QString name{};
140 QStringList tags{};
141 QIcon icon{};
142
143 PixelMatrix matrix{};
144
145 std::vector<Wheel> wheels{};
146 std::vector<FixtureMode> modes{};
147
148 static std::vector<Wheel> loadWheels(const rapidjson::Value& value) noexcept
149 {
150 std::vector<Wheel> wheels{};
151 if(!value.IsObject())
152 return wheels;
153
154 for(auto w_it = value.MemberBegin(); w_it != value.MemberEnd(); ++w_it)
155 {
156 if(!w_it->value.IsObject())
157 continue;
158
159 Wheel wh;
160 wh.name = w_it->name.GetString();
161
162 const auto& w = w_it->value.GetObject();
163 if(auto s_it = w.FindMember("slots"); s_it != w.MemberEnd())
164 {
165 if(!s_it->value.IsArray())
166 continue;
167
168 for(const auto& s : s_it->value.GetArray())
169 {
170 if(!s.IsObject())
171 continue;
172
173 WheelSlot ws;
174 if(auto nm_it = s.FindMember("name"); nm_it != s.MemberEnd())
175 {
176 if(nm_it->value.IsString())
177 ws.name = nm_it->value.GetString();
178 }
179 if(auto t_it = s.FindMember("type"); t_it != s.MemberEnd())
180 {
181 if(t_it->value.IsString())
182 ws.type = t_it->value.GetString();
183 }
184 wh.wheelslots.push_back(std::move(ws));
185 }
186 }
187
188 wheels.push_back(std::move(wh));
189 }
190
191 return wheels;
192 }
193
194 // Function returns false if the item must be filtered
195 static std::function<bool(int)> constraint_pos(std::string_view cst) noexcept
196 {
197 // Invalid constraint : we keep the item
198 if(cst.empty())
199 return [](int p) { return true; };
200
201 if(cst == "even")
202 return [](int p) { return (p % 2) == 0; };
203 else if(cst == "odd")
204 return [](int p) { return (p % 2) == 1; };
205 else if(auto [whole, n] = leq_rex(cst); whole)
206 return [num = n.to_number()](int p) { return p <= num; };
207 else if(auto [whole, n] = geq_rex(cst); whole)
208 return [num = n.to_number()](int p) { return p >= num; };
209 else if(auto [whole, n] = lst_rex(cst); whole)
210 return [num = n.to_number()](int p) { return p < num; };
211 else if(auto [whole, n] = gst_rex(cst); whole)
212 return [num = n.to_number()](int p) { return p > num; };
213 else if(auto [whole, n] = eq_rex(cst); whole)
214 return [num = n.to_number()](int p) { return p == num; };
215 else if(auto [whole, n, r] = arith_rex(cst); whole)
216 {
217 if(int num = n.to_number(); num > 0)
218 {
219 return [num, rem = r.to_number()](int p) { return (p % num) == rem; };
220 }
221 }
222 else if(auto [whole, n] = arith1_rex(cst); whole)
223 {
224 if(int num = n.to_number(); num > 0)
225 {
226 return [num](int p) { return (p % num) == 0; };
227 }
228 }
229
230 return [](int p) { return false; };
231 }
232
233 static std::function<bool(const QString&)>
234 constraint_name(std::string_view cst) noexcept
235 {
236 // Invalid constraint : we keep the item
237 if(cst.empty())
238 return [](const QString& p) { return true; };
239
240 return [rexp = std::make_shared<RE2>(re2::StringPiece(cst.data(), cst.size()))](
241 const QString& p) { return RE2::FullMatch(p.toStdString(), *rexp); };
242 }
243
244 static std::vector<int> filter_constraints(
245 const std::vector<Pixel>& pixels, const std::vector<std::string_view>& x_cst,
246 const std::vector<std::string_view>& y_cst,
247 const std::vector<std::string_view>& z_cst,
248 const std::vector<std::string_view>& name_cst) noexcept
249 {
250 std::vector<int> matching;
251 std::vector<std::function<bool(const Pixel&)>> filters;
252
253 for(auto& c : x_cst)
254 if(auto f = constraint_pos(c))
255 filters.push_back([f](const Pixel& p) { return f(p.x); });
256 for(auto& c : y_cst)
257 if(auto f = constraint_pos(c))
258 filters.push_back([f](const Pixel& p) { return f(p.y); });
259 for(auto& c : z_cst)
260 if(auto f = constraint_pos(c))
261 filters.push_back([f](const Pixel& p) { return f(p.z); });
262 for(auto& c : name_cst)
263 if(auto f = constraint_name(c))
264 filters.push_back([f](const Pixel& p) { return f(p.name); });
265
266 for(int i = 0; i < pixels.size(); i++)
267 {
268 if(pixels[i].name.isEmpty())
269 continue;
270
271 bool filtered = false;
272 for(auto& func : filters)
273 if((filtered = !func(pixels[i])))
274 break;
275
276 if(!filtered)
277 matching.push_back(i);
278 }
279
280 return matching;
281 }
282
283 void loadMatrix(const rapidjson::Value& mat)
284 {
285 using namespace std::literals;
286 // First parse the pixels
287 if(auto pk_it = mat.FindMember("pixelKeys");
288 pk_it != mat.MemberEnd() && pk_it->value.IsArray())
289 {
290 const auto& pk_z = pk_it->value.GetArray();
291 const int16_t dim_z = pk_z.Size();
292 for(int16_t z = 0; z < dim_z; ++z)
293 {
294 if(pk_z[z].IsArray())
295 {
296 const auto& pk_y = pk_z[z].GetArray();
297 const int16_t dim_y = pk_y.Size();
298 for(int16_t y = 0; y < dim_y; ++y)
299 {
300 if(pk_y[y].IsArray())
301 {
302 const auto& pk_x = pk_y[y].GetArray();
303 const int16_t dim_x = pk_x.Size();
304 for(int16_t x = 0; x < dim_x; ++x)
305 {
306 if(pk_x[x].IsString())
307 {
308 matrix.pixels.push_back(
309 Pixel{.x = x, .y = y, .z = z, .name = pk_x[x].GetString()});
310 }
311 }
312 }
313 }
314 }
315 }
316 }
317 else if(auto pc_it = mat.FindMember("pixelCount");
318 pc_it != mat.MemberEnd() && pc_it->value.IsArray())
319 {
320 const auto& pc = pc_it->value.GetArray();
321 if(pc.Size() != 3)
322 return;
323 if(!pc[0].IsInt() || !pc[1].IsInt() || !pc[2].IsInt())
324 return;
325 const int16_t dim_x = pc[0].GetInt();
326 const int16_t dim_y = pc[1].GetInt();
327 const int16_t dim_z = pc[2].GetInt();
328
329 matrix.pixels.reserve(dim_x * dim_y * dim_z);
330
331 auto get_name = (dim_y == 1 && dim_z == 1)
332 ? [](int x, int y, int z) { return QString::number(x + 1); }
333 : dim_z == 1
334 ? [](int x, int y,
335 int z) { return QString("%1 %2").arg(x + 1).arg(y + 1); }
336 : [](int x, int y, int z) {
337 return QString("%1 %2 %3").arg(x + 1).arg(y + 1).arg(z + 1);
338 };
339 for(int16_t x = 0; x < dim_x; x++)
340 for(int16_t y = 0; y < dim_y; y++)
341 for(int16_t z = 0; z < dim_z; z++)
342 matrix.pixels.push_back(
343 Pixel{.x = x, .y = y, .z = z, .name = get_name(x, y, z)});
344 }
345
346 if(matrix.pixels.empty())
347 return;
348
349 // Then parse the groups
350 if(auto gg_it = mat.FindMember("pixelGroups");
351 gg_it != mat.MemberEnd() && gg_it->value.IsObject())
352 {
353 for(auto g_it = gg_it->value.MemberBegin(); g_it != gg_it->value.MemberEnd();
354 ++g_it)
355 {
356 PixelGroup gp;
357 gp.name = g_it->name.GetString();
358
359 auto& g = g_it->value;
360
361 if(g.IsString())
362 {
363 if(g.GetString() == "all"sv)
364 {
365 // All the pixels are in the group
366 gp.pixels.reserve(matrix.pixels.size());
367 for(std::size_t i = 0; i < matrix.pixels.size(); i++)
368 gp.pixels.push_back(i);
369 }
370 }
371 else if(g.IsArray())
372 {
373 // A specific list of pixels is in the group
374 const auto& pxg = g.GetArray();
375 for(auto& px : pxg)
376 {
377 if(px.IsString())
378 {
379 QString px_name = px.GetString();
380 auto px_it = ossia::find_if(
381 matrix.pixels, [=](auto& pix) { return pix.name == px_name; });
382 if(px_it != matrix.pixels.end())
383 {
384 gp.pixels.push_back(std::distance(matrix.pixels.begin(), px_it));
385 }
386 }
387 }
388 }
389 else if(g.IsObject())
390 {
391 // Filter constraints
392 std::vector<std::string_view> x_cst, y_cst, z_cst, name_cst;
393 if(auto x_cst_it = g.FindMember("x");
394 x_cst_it != g.MemberEnd() && x_cst_it->value.IsArray())
395 {
396 for(const auto& v : x_cst_it->value.GetArray())
397 if(v.IsString())
398 x_cst.push_back(v.GetString());
399 }
400 if(auto y_cst_it = g.FindMember("y");
401 y_cst_it != g.MemberEnd() && y_cst_it->value.IsArray())
402 {
403 for(const auto& v : y_cst_it->value.GetArray())
404 if(v.IsString())
405 y_cst.push_back(v.GetString());
406 }
407 if(auto z_cst_it = g.FindMember("z");
408 z_cst_it != g.MemberEnd() && z_cst_it->value.IsArray())
409 {
410 for(const auto& v : z_cst_it->value.GetArray())
411 if(v.IsString())
412 z_cst.push_back(v.GetString());
413 }
414 if(auto n_cst_it = g.FindMember("name");
415 n_cst_it != g.MemberEnd() && n_cst_it->value.IsArray())
416 {
417 for(const auto& v : n_cst_it->value.GetArray())
418 if(v.IsString())
419 name_cst.push_back(v.GetString());
420 }
421 gp.pixels = filter_constraints(matrix.pixels, x_cst, y_cst, z_cst, name_cst);
422 }
423
424 matrix.groups.push_back(std::move(gp));
425 }
426 }
427 }
428
429 QString getWheelName(QString channelName, int wheelSlot) const noexcept
430 {
431 // It's 1-indexed...
432 wheelSlot--;
433 if(wheelSlot < 0)
434 return {};
435 for(auto& wh : this->wheels)
436 {
437 if(wh.name == channelName)
438 {
439 if(wheelSlot < std::ssize(wh.wheelslots))
440 {
441 const auto& w = wh.wheelslots[wheelSlot];
442 if(!w.name.isEmpty())
443 return w.name;
444 else if(!w.type.isEmpty())
445 return w.type;
446 }
447 break;
448 }
449 }
450
451 return {};
452 }
453
454 ChannelMap loadChannels(const rapidjson::Value& val)
455 {
456 ChannelMap channels;
457 for(auto chan_it = val.MemberBegin(); chan_it != val.MemberEnd(); ++chan_it)
458 {
459 Artnet::Channel chan;
460 chan.name = chan_it->name.GetString();
461 auto& jchan = chan_it->value;
462
463 if(jchan.IsObject())
464 {
465 if(auto default_it = jchan.FindMember("defaultValue");
466 default_it != jchan.MemberEnd())
467 {
468 if(default_it->value.IsNumber())
469 {
470 chan.defaultValue = default_it->value.GetDouble();
471 }
472 else if(default_it->value.IsString())
473 {
474 // TODO parse strings...
475 // From a quick grep in the library the only used string so far is "50%" so we optimize on that.
476 // PRs accepted :D
477 std::string_view str = default_it->value.GetString();
478 if(str == "50%")
479 chan.defaultValue = 127;
480 }
481 }
482
483 if(auto fineChannels_it = jchan.FindMember("fineChannelAliases");
484 fineChannels_it != jchan.MemberEnd())
485 {
486 const auto& fineChannels = fineChannels_it->value;
487 if(fineChannels.IsArray())
488 {
489 const auto& fc = fineChannels.GetArray();
490 for(auto& val : fc)
491 {
492 if(val.IsString())
493 {
494 chan.fineChannels.push_back(
495 QString::fromUtf8(val.GetString(), val.GetStringLength()));
496 }
497 else
498 {
499 chan.fineChannels.clear();
500 break;
501 }
502 }
503 }
504 }
505
506 if(auto capability_it = jchan.FindMember("capability");
507 capability_it != jchan.MemberEnd())
508 {
510 if(auto effectname_it = capability_it->value.FindMember("effectName");
511 effectname_it != capability_it->value.MemberEnd())
512 cap.effectName = effectname_it->value.GetString();
513
514 if(auto comment_it = capability_it->value.FindMember("comment");
515 comment_it != capability_it->value.MemberEnd())
516 cap.comment = comment_it->value.GetString();
517
518 cap.type = capability_it->value["type"].GetString();
519 chan.capabilities = std::move(cap);
520 }
521 else if(auto capabilities_it = jchan.FindMember("capabilities");
522 capabilities_it != jchan.MemberEnd())
523 {
524 std::vector<Artnet::RangeCapability> caps;
525 for(const auto& capa : capabilities_it->value.GetArray())
526 {
527 if(!capa.HasMember("type"))
528 continue;
529
530 QString type = capa["type"].GetString();
531 if(type != "NoFunction")
532 {
534 if(auto effectname_it = capa.FindMember("effectName");
535 effectname_it != capa.MemberEnd())
536 {
537 cap.effectName = effectname_it->value.GetString();
538 }
539 else if(type.startsWith("Wheel"))
540 {
541 QString whName = chan.name;
542 if(auto wh_it = capa.FindMember("wheel"); wh_it != capa.MemberEnd())
543 {
544 if(wh_it->value.IsString())
545 whName = wh_it->value.GetString();
546 }
547
548 if(auto idx_it = capa.FindMember("slotNumber");
549 idx_it != capa.MemberEnd())
550 {
551 if(idx_it->value.IsInt())
552 {
553 cap.effectName = getWheelName(whName, idx_it->value.GetInt());
554 }
555 else if(idx_it->value.IsDouble())
556 {
557 double v = idx_it->value.GetDouble();
558 double lo = std::floor(v);
559 double hi = std::ceil(v);
560 cap.effectName = QString("Split %1 %2")
561 .arg(getWheelName(whName, lo))
562 .arg(getWheelName(whName, hi));
563 }
564 }
565 }
566
567 if(auto comment_it = capa.FindMember("comment");
568 comment_it != capa.MemberEnd())
569 cap.comment = comment_it->value.GetString();
570
571 cap.type = std::move(type);
572 {
573 const auto& range_arr = capa["dmxRange"].GetArray();
574 cap.range = {range_arr[0].GetInt(), range_arr[1].GetInt()};
575 }
576 caps.push_back(std::move(cap));
577 }
578 }
579
580 chan.capabilities = std::move(caps);
581 }
582 }
583
584 channels[chan.name] = std::move(chan);
585 }
586 return channels;
587 }
588
589 void addTemplateToMode(
590 FixtureMode& m, const rapidjson::Value& repeatFor, std::string_view channelOrder,
591 const std::vector<QString>& templateChannels, const ChannelMap& templates)
592 {
593 // FIXME TODO
594 if(channelOrder != "perPixel")
595 return;
596
597 auto addTemplates = [&m, &templates, &templateChannels](const QString& pixelKey) {
598 for(const QString& channel : templateChannels)
599 {
600 // Locate the template for each channel
601 auto template_it = templates.find(channel);
602 if(template_it != templates.end())
603 {
604 QString name = template_it->first;
605 name.replace("$pixelKey", pixelKey);
606
607 // Write the channel
608 m.channels.push_back(template_it->second);
609 m.channels.back().name = name;
610 m.allChannels.push_back(name);
611 }
612 }
613 };
614
615 if(repeatFor.IsString())
616 {
617 std::string_view rf = repeatFor.GetString();
618 if(rf == "eachPixelABC")
619 {
620 auto sortedPixels = this->matrix.pixels;
621 ossia::sort(sortedPixels, [](const Pixel& p1, const Pixel& p2) {
622 return p1.name < p2.name;
623 });
624
625 for(const Pixel& pixel : sortedPixels)
626 {
627 addTemplates(pixel.name);
628 }
629 }
630 else if(rf == "eachPixelGroup")
631 {
632 for(const PixelGroup& group : this->matrix.groups)
633 {
634 addTemplates(group.name);
635 }
636 }
637 else if(ossia::string_starts_with(rf, "eachPixel"))
638 {
639 rf = rf.substr(strlen("eachPixel"));
640 if(rf.size() != 3)
641 return;
642 decltype(&Pixel::x) accessors[3];
643 for(int i = 0; i < 3; i++)
644 {
645 switch(rf[i])
646 {
647 case 'x':
648 case 'X':
649 accessors[i] = &Pixel::x;
650 break;
651 case 'y':
652 case 'Y':
653 accessors[i] = &Pixel::y;
654 break;
655 case 'z':
656 case 'Z':
657 accessors[i] = &Pixel::z;
658 break;
659 default:
660 return;
661 }
662 }
663
664 auto sortedPixels = this->matrix.pixels;
665 ossia::sort(sortedPixels, [=](const Pixel& p1, const Pixel& p2) {
666 return std::array<int16_t, 3>{
667 p1.*(accessors[2]), p1.*(accessors[1]), p1.*(accessors[0])}
668 < std::array<int16_t, 3>{
669 p2.*(accessors[2]), p2.*(accessors[1]), p2.*(accessors[0])};
670 });
671
672 for(const Pixel& pixel : sortedPixels)
673 {
674 addTemplates(pixel.name);
675 }
676 }
677 }
678 else if(repeatFor.IsArray())
679 {
680 // It's specific pixel groups or pixels
681 for(const auto& entity_v : repeatFor.GetArray())
682 {
683 if(!entity_v.IsString())
684 continue;
685
686 addTemplates(entity_v.GetString());
687 }
688 }
689 }
690
691 void loadModes(const rapidjson::Document& doc)
692 {
693 modes.clear();
694 wheels.clear();
695
696 if(auto it = doc.FindMember("wheels"); it != doc.MemberEnd())
697 if(it->value.IsObject())
698 wheels = loadWheels(it->value);
699
700 ChannelMap channels;
701 if(auto it = doc.FindMember("availableChannels"); it != doc.MemberEnd())
702 if(it->value.IsObject())
703 channels = loadChannels(it->value);
704
705 ChannelMap templateChannels;
706 if(auto it = doc.FindMember("templateChannels"); it != doc.MemberEnd())
707 if(it->value.IsObject())
708 templateChannels = loadChannels(it->value);
709
710 if(channels.empty() && templateChannels.empty())
711 return;
712
713 if(auto it = doc.FindMember("matrix"); it != doc.MemberEnd())
714 if(it->value.IsObject())
715 loadMatrix(it->value);
716
717 using namespace std::literals;
718 {
719 auto it = doc.FindMember("modes");
720 if(it == doc.MemberEnd())
721 return;
722
723 if(!it->value.IsArray())
724 return;
725
726 for(auto& mode : it->value.GetArray())
727 {
728 auto name_it = mode.FindMember("name");
729 auto channels_it = mode.FindMember("channels");
730 if(name_it != mode.MemberEnd() && channels_it != mode.MemberEnd())
731 {
732 FixtureMode m;
733 if(name_it->value.IsString())
734 m.name = name_it->value.GetString();
735
736 if(channels_it->value.IsArray())
737 {
738 for(auto& channel : channels_it->value.GetArray())
739 {
740 if(channel.IsString())
741 {
742 if(auto matched_channel_it = channels.find(channel.GetString());
743 matched_channel_it != channels.end())
744 {
745 m.channels.push_back(matched_channel_it->second);
746 }
747 else
748 {
749 // Channel is hardcoded with an existing template name
750 // FIXME maybe this happens with pixels too?
751 QString ch = channel.GetString();
752 for(const auto& tc : templateChannels)
753 {
754 for(const auto& g : this->matrix.groups)
755 {
756 QString repl = QString(tc.first).replace("$pixelKey", g.name);
757 if(repl == ch)
758 {
759 m.channels.push_back(tc.second);
760 m.channels.back().name = ch;
761 goto could_match_channel;
762 }
763 }
764 }
765 }
766 could_match_channel:
767 m.allChannels.push_back(
768 QString::fromUtf8(channel.GetString(), channel.GetStringLength()));
769 }
770 else if(channel.IsObject())
771 {
772 if(auto insert_it = channel.FindMember("insert");
773 insert_it != channel.MemberEnd() && insert_it->value.IsString()
774 && insert_it->value.GetString() == "matrixChannels"sv)
775 {
776 auto repeatFor_it = channel.FindMember("repeatFor");
777 if(repeatFor_it == channel.MemberEnd())
778 continue;
779 auto channelOrder_it = channel.FindMember("channelOrder");
780 auto templateChannels_it = channel.FindMember("templateChannels");
781 std::string channelOrder = channelOrder_it != channel.MemberEnd()
782 && channelOrder_it->value.IsString()
783 ? channelOrder_it->value.GetString()
784 : "";
785
786 std::vector<QString> modeChannels;
787 if(templateChannels_it != channel.MemberEnd()
788 && templateChannels_it->value.IsArray())
789 {
790 const auto& arr = templateChannels_it->value.GetArray();
791 for(auto& c : arr)
792 {
793 if(c.IsString())
794 modeChannels.push_back(c.GetString());
795 else
796 modeChannels.push_back({});
797 }
798 }
799
800 addTemplateToMode(
801 m, repeatFor_it->value, channelOrder, modeChannels,
802 templateChannels);
803 }
804 }
805 else
806 {
807 m.allChannels.push_back({});
808 }
809 }
810 }
811 modes.push_back(std::move(m));
812 }
813 }
814 }
815 }
816};
818
819class FixtureDatabase : public TreeNodeBasedItemModel<FixtureNode>
820{
821public:
822 std::vector<QString> m_paths;
823 struct Scan
824 {
825 explicit Scan(QString dir, FixtureNode& manufacturer) noexcept
826 : iterator{std::move(dir), QDirIterator::Subdirectories | QDirIterator::FollowSymlinks}
827 , manufacturer{manufacturer}
828 {
829 }
830 QDirIterator iterator;
831 FixtureNode& manufacturer;
832 };
833
835 : m_paths{fixturesLibraryPaths()}
836 {
837 if(!m_paths.empty())
838 {
839 for(auto& fixtures_dir : m_paths)
840 {
841 QFile f{fixtures_dir + "/manufacturers.json"};
842 if(!f.open(QIODevice::ReadOnly))
843 continue;
844 auto data = f.map(0, f.size());
845
846 rapidjson::Document doc;
847 doc.Parse(reinterpret_cast<const char*>(data), f.size());
848 if(doc.HasParseError())
849 {
850 qDebug() << "Invalid manufacturers.json !";
851 continue;
852 }
853
854 loadManufacturer(doc, fixtures_dir);
855
856 f.unmap(data);
857 }
858 }
859 }
860
861 void loadManufacturer(rapidjson::Document& doc, const QString& fixtures_dir)
862 {
863 QModelIndex rootIndex;
864 auto manufacturers = readManufacturers(doc);
865 if(m_root.childCount() == 0)
866 {
867 // Fast-path since we know that everything is already sorted
868 int k = 0;
869 for(auto it = manufacturers.begin(); it != manufacturers.end(); ++it, ++k)
870 {
871 beginInsertRows(rootIndex, k, k);
872 auto& child = m_root.emplace_back(FixtureData{it->second}, &m_root);
873 endInsertRows();
874
875 QModelIndex manufacturerIndex = createIndex(k, 0, &child);
876 nextFixture(
877 std::make_shared<Scan>(fixtures_dir + "/" + it->first, child),
878 manufacturerIndex);
879 }
880 }
881 else
882 {
883 for(auto it = manufacturers.begin(); it != manufacturers.end(); ++it)
884 {
885 auto manufacturer_node_it = ossia::find_if(
886 m_root, [&](const FixtureNode& n) { return n.name == it->second; });
887 if(manufacturer_node_it == m_root.end())
888 {
889 // We add it sorted to the model
890 int newRowPosition = 0;
891 auto other_manufacturer_it = m_root.begin();
892 while(other_manufacturer_it != m_root.end()
893 && QString::compare(
894 it->second, other_manufacturer_it->name, Qt::CaseInsensitive)
895 >= 0)
896 {
897 other_manufacturer_it++;
898 newRowPosition++;
899 }
900
901 beginInsertRows(rootIndex, newRowPosition, newRowPosition);
902 auto& child
903 = m_root.emplace(other_manufacturer_it, FixtureData{it->second}, &m_root);
904 endInsertRows();
905
906 QModelIndex manufacturerIndex = createIndex(newRowPosition, 0, &child);
907 nextFixture(
908 std::make_shared<Scan>(fixtures_dir + "/" + it->first, child),
909 manufacturerIndex);
910 }
911 else
912 {
913 int distance = std::abs(std::distance(manufacturer_node_it, m_root.begin()));
914 QModelIndex manufacturerIndex
915 = createIndex(distance, 0, &*manufacturer_node_it);
916 nextFixture(
917 std::make_shared<Scan>(
918 fixtures_dir + "/" + it->first, *manufacturer_node_it),
919 manufacturerIndex);
920 }
921 }
922 }
923 }
924
925 void loadFixture(
926 std::string_view fixture_data, FixtureNode& manufacturer,
927 const QModelIndex& manufacturerIndex)
928 {
929 rapidjson::Document doc;
930 doc.Parse(fixture_data.data(), fixture_data.size());
931 if(doc.HasParseError())
932 {
933 qDebug() << "Invalid JSON document !";
934 return;
935 }
936 if(auto it = doc.FindMember("name"); it != doc.MemberEnd())
937 {
938 QString name = it->value.GetString();
939
940 int newRowPosition = 0;
941 auto other_fixture_it = manufacturer.begin();
942 while(other_fixture_it != manufacturer.end()
943 && QString::compare(name, other_fixture_it->name, Qt::CaseInsensitive) >= 0)
944 {
945 other_fixture_it++;
946 newRowPosition++;
947 }
948
949 beginInsertRows(manufacturerIndex, newRowPosition, newRowPosition);
950 auto& data
951 = manufacturer.emplace(other_fixture_it, FixtureData{name}, &manufacturer);
952 endInsertRows();
953
954 data.loadModes(doc);
955
956 if(auto it = doc.FindMember("categories"); it != doc.MemberEnd())
957 {
958 for(auto& category : it->value.GetArray())
959 {
960 data.tags.push_back(category.GetString());
961 }
962 }
963 }
964 }
965
966 // Note: we could use make_unique here but on old Ubuntus stdlibc++-7 does not seem to support it (or Qt 5.9)
967 // -> QTimer::singleShot calls copy ctor
968 void nextFixture(std::shared_ptr<Scan> scan, QModelIndex manufacturerIndex)
969 {
970 auto& iterator = scan->iterator;
971 if(iterator.hasNext())
972 {
973 const auto filepath = iterator.next();
974 if(QFileInfo fi{filepath}; fi.suffix() == "json")
975 {
976 const std::string_view req{"fixture.json"};
977 score::findStringInFile(filepath, req, [&](QFile& f) {
978 unsigned char* data = f.map(0, f.size());
979
980 const char* cbegin = reinterpret_cast<char*>(data);
981
982 loadFixture(
983 std::string_view(cbegin, f.size()), scan->manufacturer, manufacturerIndex);
984 });
985 }
986
987 m_inFlight++;
988 QTimer::singleShot(
989 1, this, [this, scan = std::move(scan), idx = manufacturerIndex]() mutable {
990 nextFixture(std::move(scan), idx);
991 m_inFlight--;
992 });
993 }
994 }
995
996 FixtureNode& rootNode() override { return m_root; }
997
998 const FixtureNode& rootNode() const override { return m_root; }
999
1000 int columnCount(const QModelIndex& parent) const override { return 2; }
1001
1002 QVariant data(const QModelIndex& index, int role) const override
1003 {
1004 const auto& node = nodeFromModelIndex(index);
1005 if(index.column() == 0)
1006 {
1007 switch(role)
1008 {
1009 case Qt::DisplayRole:
1010 return node.name;
1011 // case Qt::DecorationRole:
1012 // return node.icon;
1013 }
1014 }
1015 else if(index.column() == 1)
1016 {
1017 switch(role)
1018 {
1019 case Qt::DisplayRole:
1020 return node.tags.join(", ");
1021 }
1022 }
1023 return QVariant{};
1024 }
1025
1026 QVariant headerData(int section, Qt::Orientation orientation, int role) const override
1027 {
1028 if(role != Qt::DisplayRole)
1029 return TreeNodeBasedItemModel<FixtureNode>::headerData(section, orientation, role);
1030 switch(section)
1031 {
1032 case 0:
1033 return tr("Fixture");
1034 case 1:
1035 return tr("Tags");
1036 default:
1037 return {};
1038 }
1039 }
1040 Qt::ItemFlags flags(const QModelIndex& index) const override
1041 {
1042 Qt::ItemFlags f;
1043
1044 f = Qt::ItemIsEnabled | Qt::ItemIsSelectable;
1045
1046 return f;
1047 }
1048
1049 QModelIndex modelIndexFromNode(const FixtureNode& n) const
1050 {
1051 node_type* parent = n.parent();
1052 SCORE_ASSERT(parent);
1053 SCORE_ASSERT(parent != &rootNode());
1054
1055 return createIndex(parent->indexOfChild(&n), 0, &n);
1056 }
1057
1058 static FixtureDatabase& instance()
1059 {
1060 static FixtureDatabase db;
1061 return db;
1062 }
1063
1064 void onPopulated(auto func)
1065 {
1066 if(m_inFlight == 0)
1067 {
1068 func();
1069 }
1070 else
1071 {
1072 QTimer::singleShot(8, this, [this, func = std::move(func)]() mutable {
1073 onPopulated(std::move(func));
1074 });
1075 }
1076 }
1077
1078 FixtureNode m_root;
1079
1080 int m_inFlight{};
1081};
1082
1083}
Definition LibrarySettings.hpp:46
Definition FixtureDatabase.hpp:136
Definition FixtureDatabase.hpp:820
Definition TreeNodeItemModel.hpp:38
Definition ArtnetSpecificSettings.hpp:86
Definition ArtnetSpecificSettings.hpp:30
Definition ArtnetSpecificSettings.hpp:27
Definition FixtureDatabase.hpp:824
Definition FixtureDatabase.hpp:102
Definition FixtureDatabase.hpp:90
Definition FixtureDatabase.hpp:84
Definition FixtureDatabase.hpp:96
Definition FixtureDatabase.hpp:131
Definition FixtureDatabase.hpp:125
T & settings() const
Access a specific Settings model instance.
Definition ApplicationContext.hpp:40