blob: e09a86efbdc5a1111530b43459e766586f1b3071 [file] [log] [blame]
Austin Schuh812d0d12021-11-04 20:16:48 -07001// Copyright (c) FIRST and other WPILib contributors.
2// Open Source Software; you can modify and/or share it under the terms of
3// the WPILib BSD license file in the root directory of this project.
4
5#include "glass/Context.h"
6
7#include <algorithm>
8#include <cinttypes>
Austin Schuh75263e32022-02-22 18:05:32 -08009#include <filesystem>
Austin Schuh812d0d12021-11-04 20:16:48 -070010
Austin Schuh75263e32022-02-22 18:05:32 -080011#include <fmt/format.h>
Austin Schuh812d0d12021-11-04 20:16:48 -070012#include <imgui.h>
13#include <imgui_internal.h>
14#include <imgui_stdlib.h>
James Kuszmaulb13e13f2023-11-22 20:44:04 -080015#include <wpi/MemoryBuffer.h>
Austin Schuh812d0d12021-11-04 20:16:48 -070016#include <wpi/StringExtras.h>
Austin Schuh75263e32022-02-22 18:05:32 -080017#include <wpi/fs.h>
18#include <wpi/json.h>
Austin Schuh75263e32022-02-22 18:05:32 -080019#include <wpi/raw_ostream.h>
Austin Schuh812d0d12021-11-04 20:16:48 -070020#include <wpi/timestamp.h>
21#include <wpigui.h>
Austin Schuh75263e32022-02-22 18:05:32 -080022#include <wpigui_internal.h>
Austin Schuh812d0d12021-11-04 20:16:48 -070023
24#include "glass/ContextInternal.h"
25
26using namespace glass;
27
28Context* glass::gContext;
29
Austin Schuh75263e32022-02-22 18:05:32 -080030static void WorkspaceResetImpl() {
31 // call reset functions
32 for (auto&& reset : gContext->workspaceReset) {
33 if (reset) {
34 reset();
35 }
Austin Schuh812d0d12021-11-04 20:16:48 -070036 }
Austin Schuh75263e32022-02-22 18:05:32 -080037
38 // clear storage
39 for (auto&& root : gContext->storageRoots) {
40 root.second->Clear();
41 }
42
43 // ImGui reset
44 ImGui::ClearIniSettings();
Austin Schuh812d0d12021-11-04 20:16:48 -070045}
46
Austin Schuh75263e32022-02-22 18:05:32 -080047static void WorkspaceInit() {
48 for (auto&& init : gContext->workspaceInit) {
49 if (init) {
50 init();
51 }
Austin Schuh812d0d12021-11-04 20:16:48 -070052 }
Austin Schuh75263e32022-02-22 18:05:32 -080053
54 for (auto&& root : gContext->storageRoots) {
55 root.getValue()->Apply();
56 }
Austin Schuh812d0d12021-11-04 20:16:48 -070057}
58
Austin Schuh75263e32022-02-22 18:05:32 -080059static bool JsonToWindow(const wpi::json& jfile, const char* filename) {
60 if (!jfile.is_object()) {
61 ImGui::LogText("%s top level is not object", filename);
62 return false;
Austin Schuh812d0d12021-11-04 20:16:48 -070063 }
Austin Schuh75263e32022-02-22 18:05:32 -080064
65 // loop over JSON and generate ini format
66 std::string iniStr;
67 wpi::raw_string_ostream ini{iniStr};
68
69 for (auto&& jsection : jfile.items()) {
James Kuszmaulcf324122023-01-14 14:07:17 -080070 if (jsection.key() == "Docking") {
71 continue;
72 }
Austin Schuh75263e32022-02-22 18:05:32 -080073 if (!jsection.value().is_object()) {
74 ImGui::LogText("%s section %s is not object", filename,
75 jsection.key().c_str());
76 return false;
77 }
78 for (auto&& jsubsection : jsection.value().items()) {
79 if (!jsubsection.value().is_object()) {
80 ImGui::LogText("%s section %s subsection %s is not object", filename,
81 jsection.key().c_str(), jsubsection.key().c_str());
82 return false;
83 }
84 ini << '[' << jsection.key() << "][" << jsubsection.key() << "]\n";
85 for (auto&& jkv : jsubsection.value().items()) {
86 try {
87 auto& value = jkv.value().get_ref<const std::string&>();
88 ini << jkv.key() << '=' << value << "\n";
89 } catch (wpi::json::exception&) {
90 ImGui::LogText("%s section %s subsection %s value %s is not string",
91 filename, jsection.key().c_str(),
92 jsubsection.key().c_str(), jkv.key().c_str());
93 return false;
94 }
95 }
96 ini << '\n';
97 }
98 }
James Kuszmaulcf324122023-01-14 14:07:17 -080099
100 // emit Docking section last
101 auto docking = jfile.find("Docking");
102 if (docking != jfile.end()) {
103 for (auto&& jsubsection : docking->items()) {
104 if (!jsubsection.value().is_array()) {
105 ImGui::LogText("%s section %s subsection %s is not array", filename,
106 "Docking", jsubsection.key().c_str());
107 return false;
108 }
109 ini << "[Docking][" << jsubsection.key() << "]\n";
110 for (auto&& jv : jsubsection.value()) {
111 try {
112 auto& value = jv.get_ref<const std::string&>();
113 ini << value << "\n";
114 } catch (wpi::json::exception&) {
115 ImGui::LogText("%s section %s subsection %s value is not string",
116 filename, "Docking", jsubsection.key().c_str());
117 return false;
118 }
119 }
120 ini << '\n';
121 }
122 }
123
Austin Schuh75263e32022-02-22 18:05:32 -0800124 ini.flush();
125
126 ImGui::LoadIniSettingsFromMemory(iniStr.data(), iniStr.size());
127 return true;
Austin Schuh812d0d12021-11-04 20:16:48 -0700128}
129
Austin Schuh75263e32022-02-22 18:05:32 -0800130static bool LoadWindowStorageImpl(const std::string& filename) {
131 std::error_code ec;
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800132 std::unique_ptr<wpi::MemoryBuffer> fileBuffer =
133 wpi::MemoryBuffer::GetFile(filename, ec);
134 if (fileBuffer == nullptr || ec) {
Austin Schuh75263e32022-02-22 18:05:32 -0800135 ImGui::LogText("error opening %s: %s", filename.c_str(),
136 ec.message().c_str());
137 return false;
Austin Schuh812d0d12021-11-04 20:16:48 -0700138 } else {
Austin Schuh75263e32022-02-22 18:05:32 -0800139 try {
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800140 return JsonToWindow(
141 wpi::json::parse(fileBuffer->begin(), fileBuffer->end()),
142 filename.c_str());
Austin Schuh75263e32022-02-22 18:05:32 -0800143 } catch (wpi::json::parse_error& e) {
144 ImGui::LogText("Error loading %s: %s", filename.c_str(), e.what());
145 return false;
Austin Schuh812d0d12021-11-04 20:16:48 -0700146 }
147 }
148}
149
Austin Schuh75263e32022-02-22 18:05:32 -0800150static bool LoadStorageRootImpl(Context* ctx, const std::string& filename,
151 std::string_view rootName) {
152 std::error_code ec;
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800153 std::unique_ptr<wpi::MemoryBuffer> fileBuffer =
154 wpi::MemoryBuffer::GetFile(filename, ec);
155 if (fileBuffer == nullptr || ec) {
Austin Schuh75263e32022-02-22 18:05:32 -0800156 ImGui::LogText("error opening %s: %s", filename.c_str(),
157 ec.message().c_str());
158 return false;
159 } else {
160 auto& storage = ctx->storageRoots[rootName];
161 bool createdStorage = false;
162 if (!storage) {
163 storage = std::make_unique<Storage>();
164 createdStorage = true;
165 }
166 try {
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800167 storage->FromJson(
168 wpi::json::parse(fileBuffer->begin(), fileBuffer->end()),
169 filename.c_str());
Austin Schuh75263e32022-02-22 18:05:32 -0800170 } catch (wpi::json::parse_error& e) {
171 ImGui::LogText("Error loading %s: %s", filename.c_str(), e.what());
172 if (createdStorage) {
173 ctx->storageRoots.erase(rootName);
174 }
175 return false;
176 }
Austin Schuh812d0d12021-11-04 20:16:48 -0700177 }
Austin Schuh75263e32022-02-22 18:05:32 -0800178 return true;
179}
Austin Schuh812d0d12021-11-04 20:16:48 -0700180
Austin Schuh75263e32022-02-22 18:05:32 -0800181static bool LoadStorageImpl(Context* ctx, std::string_view dir,
182 std::string_view name) {
183 WorkspaceResetImpl();
184
185 bool rv = true;
186 for (auto&& root : ctx->storageRoots) {
187 std::string filename;
188 auto rootName = root.getKey();
189 if (rootName.empty()) {
190 filename = (fs::path{dir} / fmt::format("{}.json", name)).string();
191 } else {
192 filename =
193 (fs::path{dir} / fmt::format("{}-{}.json", name, rootName)).string();
194 }
195 if (!LoadStorageRootImpl(ctx, filename, rootName)) {
196 rv = false;
197 }
198 }
199
200 WorkspaceInit();
201 return rv;
202}
203
204static wpi::json WindowToJson() {
205 size_t iniLen;
206 const char* iniData = ImGui::SaveIniSettingsToMemory(&iniLen);
207 std::string_view ini{iniData, iniLen};
208
209 // parse the ini data and build JSON
210 // JSON format:
211 // {
212 // "Section": {
213 // "Subsection": {
214 // "Key": "Value" // all values are saved as strings
215 // }
216 // }
217 // }
218
219 wpi::json out = wpi::json::object();
220 wpi::json* curSection = nullptr;
221 while (!ini.empty()) {
222 std::string_view line;
223 std::tie(line, ini) = wpi::split(ini, '\n');
224 line = wpi::trim(line);
225 if (line.empty()) {
226 continue;
227 }
228 if (line[0] == '[') {
229 // new section
230 auto [section, subsection] = wpi::split(line, ']');
231 section = wpi::drop_front(section); // drop '['; ']' was dropped by split
232 subsection = wpi::drop_back(wpi::drop_front(subsection)); // drop []
233 auto& jsection = out[section];
234 if (jsection.is_null()) {
235 jsection = wpi::json::object();
236 }
237 curSection = &jsection[subsection];
238 if (curSection->is_null()) {
James Kuszmaulcf324122023-01-14 14:07:17 -0800239 if (section == "Docking") {
240 *curSection = wpi::json::array();
241 } else {
242 *curSection = wpi::json::object();
243 }
Austin Schuh75263e32022-02-22 18:05:32 -0800244 }
245 } else {
246 // value
247 if (!curSection) {
248 continue; // shouldn't happen, but just in case
249 }
250 auto [name, value] = wpi::split(line, '=');
James Kuszmaulcf324122023-01-14 14:07:17 -0800251 if (curSection->is_object()) {
252 (*curSection)[name] = value;
253 } else if (curSection->is_array()) {
254 curSection->emplace_back(line);
255 }
Austin Schuh75263e32022-02-22 18:05:32 -0800256 }
257 }
258
259 return out;
260}
261
262bool SaveWindowStorageImpl(const std::string& filename) {
263 std::error_code ec;
264 wpi::raw_fd_ostream os{filename, ec};
265 if (ec) {
266 ImGui::LogText("error opening %s: %s", filename.c_str(),
267 ec.message().c_str());
268 return false;
269 }
270 WindowToJson().dump(os, 2);
271 os << '\n';
272 return true;
273}
274
275static bool SaveStorageRootImpl(Context* ctx, const std::string& filename,
276 const Storage& storage) {
277 std::error_code ec;
278 wpi::raw_fd_ostream os{filename, ec};
279 if (ec) {
280 ImGui::LogText("error opening %s: %s", filename.c_str(),
281 ec.message().c_str());
282 return false;
283 }
284 storage.ToJson().dump(os, 2);
285 os << '\n';
286 return true;
287}
288
289static bool SaveStorageImpl(Context* ctx, std::string_view dir,
290 std::string_view name, bool exiting) {
291 fs::path dirPath{dir};
292
293 std::error_code ec;
294 fs::create_directories(dirPath, ec);
295 if (ec) {
296 return false;
297 }
298
299 // handle erasing save files on exit if requested
300 if (exiting && wpi::gui::gContext->resetOnExit) {
301 fs::remove(dirPath / fmt::format("{}-window.json", name), ec);
302 for (auto&& root : ctx->storageRoots) {
303 auto rootName = root.getKey();
304 if (rootName.empty()) {
305 fs::remove(dirPath / fmt::format("{}.json", name), ec);
306 } else {
307 fs::remove(dirPath / fmt::format("{}-{}.json", name, rootName), ec);
Austin Schuh812d0d12021-11-04 20:16:48 -0700308 }
309 }
Austin Schuh812d0d12021-11-04 20:16:48 -0700310 }
Austin Schuh75263e32022-02-22 18:05:32 -0800311
312 bool rv = SaveWindowStorageImpl(
313 (dirPath / fmt::format("{}-window.json", name)).string());
314
315 for (auto&& root : ctx->storageRoots) {
316 auto rootName = root.getKey();
317 std::string filename;
318 if (rootName.empty()) {
319 filename = (dirPath / fmt::format("{}.json", name)).string();
320 } else {
321 filename = (dirPath / fmt::format("{}-{}.json", name, rootName)).string();
322 }
323 if (!SaveStorageRootImpl(ctx, filename, *root.getValue())) {
324 rv = false;
325 }
326 }
327 return rv;
Austin Schuh812d0d12021-11-04 20:16:48 -0700328}
329
Austin Schuh75263e32022-02-22 18:05:32 -0800330Context::Context()
331 : sourceNameStorage{storageRoots.insert({"", std::make_unique<Storage>()})
332 .first->getValue()
333 ->GetChild("sourceNames")} {
334 storageStack.emplace_back(storageRoots[""].get());
Austin Schuh812d0d12021-11-04 20:16:48 -0700335
Austin Schuh75263e32022-02-22 18:05:32 -0800336 // override ImGui ini saving
337 wpi::gui::ConfigureCustomSaveSettings(
338 [this] { LoadStorageImpl(this, storageLoadDir, storageName); },
339 [this] {
340 LoadWindowStorageImpl((fs::path{storageLoadDir} /
341 fmt::format("{}-window.json", storageName))
342 .string());
343 },
344 [this](bool exiting) {
345 SaveStorageImpl(this, storageAutoSaveDir, storageName, exiting);
346 });
Austin Schuh812d0d12021-11-04 20:16:48 -0700347}
348
Austin Schuh75263e32022-02-22 18:05:32 -0800349Context::~Context() {
350 wpi::gui::ConfigureCustomSaveSettings(nullptr, nullptr, nullptr);
351}
Austin Schuh812d0d12021-11-04 20:16:48 -0700352
353Context* glass::CreateContext() {
354 Context* ctx = new Context;
355 if (!gContext) {
356 SetCurrentContext(ctx);
357 }
Austin Schuh812d0d12021-11-04 20:16:48 -0700358 return ctx;
359}
360
361void glass::DestroyContext(Context* ctx) {
362 if (!ctx) {
363 ctx = gContext;
364 }
Austin Schuh812d0d12021-11-04 20:16:48 -0700365 if (gContext == ctx) {
366 SetCurrentContext(nullptr);
367 }
368 delete ctx;
369}
370
371Context* glass::GetCurrentContext() {
372 return gContext;
373}
374
375void glass::SetCurrentContext(Context* ctx) {
376 gContext = ctx;
377}
378
379void glass::ResetTime() {
380 gContext->zeroTime = wpi::Now();
381}
382
383uint64_t glass::GetZeroTime() {
384 return gContext->zeroTime;
385}
386
Austin Schuh75263e32022-02-22 18:05:32 -0800387void glass::WorkspaceReset() {
388 WorkspaceResetImpl();
389 WorkspaceInit();
390}
391
392void glass::AddWorkspaceInit(std::function<void()> init) {
393 if (init) {
394 gContext->workspaceInit.emplace_back(std::move(init));
Austin Schuh812d0d12021-11-04 20:16:48 -0700395 }
396}
397
Austin Schuh75263e32022-02-22 18:05:32 -0800398void glass::AddWorkspaceReset(std::function<void()> reset) {
399 if (reset) {
400 gContext->workspaceReset.emplace_back(std::move(reset));
Austin Schuh812d0d12021-11-04 20:16:48 -0700401 }
402}
403
Austin Schuh75263e32022-02-22 18:05:32 -0800404void glass::SetStorageName(std::string_view name) {
405 gContext->storageName = name;
406}
407
408void glass::SetStorageDir(std::string_view dir) {
409 if (dir.empty()) {
410 gContext->storageLoadDir = ".";
411 gContext->storageAutoSaveDir = ".";
Austin Schuh812d0d12021-11-04 20:16:48 -0700412 } else {
Austin Schuh75263e32022-02-22 18:05:32 -0800413 gContext->storageLoadDir = dir;
414 gContext->storageAutoSaveDir = dir;
415 gContext->isPlatformSaveDir = (dir == wpi::gui::GetPlatformSaveFileDir());
Austin Schuh812d0d12021-11-04 20:16:48 -0700416 }
417}
418
Austin Schuh75263e32022-02-22 18:05:32 -0800419std::string glass::GetStorageDir() {
420 return gContext->storageAutoSaveDir;
421}
422
423bool glass::LoadStorage(std::string_view dir) {
424 SaveStorage();
425 SetStorageDir(dir);
426 LoadWindowStorageImpl((fs::path{gContext->storageLoadDir} /
427 fmt::format("{}-window.json", gContext->storageName))
428 .string());
429 return LoadStorageImpl(gContext, dir, gContext->storageName);
430}
431
432bool glass::SaveStorage() {
433 return SaveStorageImpl(gContext, gContext->storageAutoSaveDir,
434 gContext->storageName, false);
435}
436
437bool glass::SaveStorage(std::string_view dir) {
438 return SaveStorageImpl(gContext, dir, gContext->storageName, false);
439}
440
441Storage& glass::GetCurStorageRoot() {
442 return *gContext->storageStack.front();
443}
444
445Storage& glass::GetStorageRoot(std::string_view rootName) {
446 auto& storage = gContext->storageRoots[rootName];
447 if (!storage) {
448 storage = std::make_unique<Storage>();
449 }
450 return *storage;
451}
452
453void glass::ResetStorageStack(std::string_view rootName) {
454 if (gContext->storageStack.size() != 1) {
455 ImGui::LogText("resetting non-empty storage stack");
456 }
457 gContext->storageStack.clear();
458 gContext->storageStack.emplace_back(&GetStorageRoot(rootName));
459}
460
Austin Schuh812d0d12021-11-04 20:16:48 -0700461Storage& glass::GetStorage() {
Austin Schuh75263e32022-02-22 18:05:32 -0800462 return *gContext->storageStack.back();
Austin Schuh812d0d12021-11-04 20:16:48 -0700463}
464
Austin Schuh75263e32022-02-22 18:05:32 -0800465void glass::PushStorageStack(std::string_view label_id) {
466 gContext->storageStack.emplace_back(
467 &gContext->storageStack.back()->GetChild(label_id));
Austin Schuh812d0d12021-11-04 20:16:48 -0700468}
469
Austin Schuh75263e32022-02-22 18:05:32 -0800470void glass::PushStorageStack(Storage& storage) {
471 gContext->storageStack.emplace_back(&storage);
Austin Schuh812d0d12021-11-04 20:16:48 -0700472}
473
Austin Schuh75263e32022-02-22 18:05:32 -0800474void glass::PopStorageStack() {
475 if (gContext->storageStack.size() <= 1) {
476 ImGui::LogText("attempted to pop empty storage stack, mismatch push/pop?");
477 return; // ignore
478 }
479 gContext->storageStack.pop_back();
Austin Schuh812d0d12021-11-04 20:16:48 -0700480}
481
482bool glass::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) {
Austin Schuh75263e32022-02-22 18:05:32 -0800483 PushStorageStack(name);
Austin Schuh812d0d12021-11-04 20:16:48 -0700484 return ImGui::Begin(name, p_open, flags);
485}
486
487void glass::End() {
488 ImGui::End();
Austin Schuh75263e32022-02-22 18:05:32 -0800489 PopStorageStack();
Austin Schuh812d0d12021-11-04 20:16:48 -0700490}
491
492bool glass::BeginChild(const char* str_id, const ImVec2& size, bool border,
493 ImGuiWindowFlags flags) {
Austin Schuh75263e32022-02-22 18:05:32 -0800494 PushStorageStack(str_id);
Austin Schuh812d0d12021-11-04 20:16:48 -0700495 return ImGui::BeginChild(str_id, size, border, flags);
496}
497
498void glass::EndChild() {
499 ImGui::EndChild();
Austin Schuh75263e32022-02-22 18:05:32 -0800500 PopStorageStack();
Austin Schuh812d0d12021-11-04 20:16:48 -0700501}
502
503bool glass::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags) {
Austin Schuh75263e32022-02-22 18:05:32 -0800504 bool& open = GetStorage().GetChild(label).GetBool(
505 "open", (flags & ImGuiTreeNodeFlags_DefaultOpen) != 0);
506 ImGui::SetNextItemOpen(open);
507 open = ImGui::CollapsingHeader(label, flags);
508 return open;
Austin Schuh812d0d12021-11-04 20:16:48 -0700509}
510
511bool glass::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags) {
Austin Schuh75263e32022-02-22 18:05:32 -0800512 PushStorageStack(label);
513 bool& open = GetStorage().GetBool(
514 "open", (flags & ImGuiTreeNodeFlags_DefaultOpen) != 0);
515 ImGui::SetNextItemOpen(open);
516 open = ImGui::TreeNodeEx(label, flags);
517 if (!open) {
518 PopStorageStack();
Austin Schuh812d0d12021-11-04 20:16:48 -0700519 }
Austin Schuh75263e32022-02-22 18:05:32 -0800520 return open;
Austin Schuh812d0d12021-11-04 20:16:48 -0700521}
522
523void glass::TreePop() {
524 ImGui::TreePop();
Austin Schuh75263e32022-02-22 18:05:32 -0800525 PopStorageStack();
Austin Schuh812d0d12021-11-04 20:16:48 -0700526}
527
528void glass::PushID(const char* str_id) {
Austin Schuh75263e32022-02-22 18:05:32 -0800529 PushStorageStack(str_id);
Austin Schuh812d0d12021-11-04 20:16:48 -0700530 ImGui::PushID(str_id);
531}
532
533void glass::PushID(const char* str_id_begin, const char* str_id_end) {
Austin Schuh75263e32022-02-22 18:05:32 -0800534 PushStorageStack(std::string_view(str_id_begin, str_id_end - str_id_begin));
Austin Schuh812d0d12021-11-04 20:16:48 -0700535 ImGui::PushID(str_id_begin, str_id_end);
536}
537
538void glass::PushID(int int_id) {
539 char buf[16];
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800540 wpi::format_to_n_c_str(buf, sizeof(buf), "{}", int_id);
541
Austin Schuh75263e32022-02-22 18:05:32 -0800542 PushStorageStack(buf);
Austin Schuh812d0d12021-11-04 20:16:48 -0700543 ImGui::PushID(int_id);
544}
545
546void glass::PopID() {
547 ImGui::PopID();
Austin Schuh75263e32022-02-22 18:05:32 -0800548 PopStorageStack();
Austin Schuh812d0d12021-11-04 20:16:48 -0700549}
550
551bool glass::PopupEditName(const char* label, std::string* name) {
552 bool rv = false;
553 if (ImGui::BeginPopupContextItem(label)) {
James Kuszmaulcf324122023-01-14 14:07:17 -0800554 rv = ItemEditName(name);
555
Austin Schuh812d0d12021-11-04 20:16:48 -0700556 ImGui::EndPopup();
557 }
558 return rv;
559}
James Kuszmaulcf324122023-01-14 14:07:17 -0800560
561bool glass::ItemEditName(std::string* name) {
562 bool rv = false;
563
564 ImGui::Text("Edit name:");
565 if (ImGui::InputText("##editname", name)) {
566 rv = true;
567 }
568 if (ImGui::Button("Close") || ImGui::IsKeyPressedMap(ImGuiKey_Enter) ||
569 ImGui::IsKeyPressedMap(ImGuiKey_KeyPadEnter)) {
570 ImGui::CloseCurrentPopup();
571 }
572
573 return rv;
574}