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