MNX Document Model
Loading...
Searching...
No Matches
EntityMap.h
1/*
2 * Copyright (C) 2025, Robert Patterson
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a copy
5 * of this software and associated documentation files (the "Software"), to deal
6 * in the Software without restriction, including without limitation the rights
7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 * copies of the Software, and to permit persons to whom the Software is
9 * furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 * THE SOFTWARE.
21 */
22#pragma once
23
24#include <algorithm>
25#include <array>
26#include <memory>
27#include <optional>
28#include <sstream>
29#include <string>
30#include <tuple>
31#include <type_traits>
32#include <unordered_map>
33
34#include "../BaseTypes.h"
35#include "../Global.h"
36#include "../Layout.h"
37#include "../Part.h"
38#include "../Score.h"
39#include "../Sequence.h"
40
41namespace mnx::util {
42
43namespace detail {
44#ifndef DOXYGEN_SHOULD_IGNORE_THIS
45
46template <typename T>
47struct JsonSchemaTypeNames
48{
49 static constexpr std::array<std::string_view, 1> value{ T::JsonSchemaTypeName };
50};
51
52template <>
53struct JsonSchemaTypeNames<sequence::NoteBase>
54{
55 static constexpr std::array<std::string_view, 2> value{
56 sequence::Note::JsonSchemaTypeName,
57 sequence::KitNote::JsonSchemaTypeName
58 };
59};
60
61template <typename T>
62bool matchesTypeName(std::string_view typeName)
63{
64 for (const auto name : JsonSchemaTypeNames<T>::value) {
65 if (name == typeName) {
66 return true;
67 }
68 }
69 return false;
70}
71
72template <typename T>
73std::string typeNamesExpectationString()
74{
75 std::ostringstream oss;
76 const auto& names = JsonSchemaTypeNames<T>::value;
77 const auto count = names.size();
78 if (count > 1) {
79 oss << "expected one of ";
80 } else {
81 oss << "expected ";
82 }
83 for (size_t i = 0; i < count; ++i) {
84 if (i > 0) {
85 oss << ", ";
86 }
87 oss << "\"" << names[i] << "\"";
88 }
89 return oss.str();
90}
91
92#endif // DOXYGEN_SHOULD_IGNORE_THIS
93} // namespace detail
94
96class mapping_error : public std::runtime_error
97{
98 using runtime_error::runtime_error;
99};
100
109{
110private:
116 template <typename T>
117 void add(const std::string& id, const T& value)
118 {
119 auto result = m_objectMap.emplace(id, MappedLocation{ value.pointer(), T::JsonSchemaTypeName });
120 if (!result.second) {
121 mapping_error err("ID " + formatKeyString(id) + " already exists for type \"" + std::string(result.first->second.typeName)
122 + "\" at " + result.first->second.location.to_string());
123 if (m_errorHandler) {
124 m_errorHandler.value()(err.what(), value);
125 } else {
126 throw err;
127 }
128 }
129 }
130
134 void addEventToBeam(const std::string& eventId, const part::Beam& beam)
135 {
136 auto result = m_eventsInBeams.emplace(eventId, BeamMappingEntry{ MappedLocation{ beam.pointer(), part::Beam::JsonSchemaTypeName }, 0 });
137 if (!result.second) {
138 mapping_error err("ID " + formatKeyString(eventId) + " already exists in beam "
139 + result.first->second.location.location.to_string());
140 if (m_errorHandler) {
141 m_errorHandler.value()(err.what(), beam);
142 } else {
143 throw err;
144 }
145 }
146 }
147
149 void setEventBeamStartLevel(const std::string& eventId, int level)
150 {
151 auto it = m_eventsInBeams.find(eventId);
152 if (it == m_eventsInBeams.end()) {
153 throw std::logic_error("Attempted to assign a beam start level to unmapped event " + eventId);
154 }
155 if (it->second.startLevel == 0) {
156 it->second.startLevel = level;
157 } else {
158 it->second.startLevel = std::min(it->second.startLevel, level);
159 }
160 }
161
163 void setEventOttavaShift(const std::string& eventPointer, int shift)
164 {
165 m_eventOttavaShift[eventPointer] = shift;
166 }
167
168 friend class mnx::Document;
169
170public:
176 explicit EntityMap(std::weak_ptr<json> documentRoot, const std::optional<ErrorHandler>& errorHandler = std::nullopt)
177 : m_root(documentRoot), m_errorHandler(errorHandler) {}
178
192 template <typename T>
193 std::optional<T> tryGet(
194 const std::string& id,
195 const std::optional<Base>& errorLocation = std::nullopt) const
196 {
197 auto it = m_objectMap.find(id);
198 if (it == m_objectMap.end()) {
199 return std::nullopt;
200 }
201 MNX_ASSERT_IF(!detail::matchesTypeName<T>(it->second.typeName)) {
202 mapping_error err(
203 "ID " + formatKeyString(id) + " has type \"" + std::string(it->second.typeName) +
204 "\", but " + detail::typeNamesExpectationString<T>() + "."
205 );
206 if (m_errorHandler) {
207 m_errorHandler.value()(err.what(), errorLocation.value_or(Document(root())));
208 }
209 throw err;
210 }
211 return T(root(), it->second.location);
212 }
213
222 template <typename T>
223 T get(const std::string& id, const std::optional<Base>& errorLocation = std::nullopt) const
224 {
225 if (auto v = tryGet<T>(id, errorLocation)) {
226 return *std::move(v);
227 }
228 mapping_error err("ID " + formatKeyString(id) + " not found in ID mapping");
229 if (m_errorHandler) {
230 m_errorHandler.value()(err.what(), errorLocation.value_or(Document(root())));
231 }
232 throw err;
233 }
234
246 template <typename T>
247 size_t getIndexOf(const std::string& id, const std::optional<Base>& errorLocation = std::nullopt) const
248 {
249 static_assert(std::is_base_of_v<ArrayElementObject, T>,
250 "getIndexOf<T> requires T to derive from ArrayElementObject");
251
252 auto v = get<T>(id, errorLocation);
253 return v.calcArrayIndex();
254 }
255
256
258 template <typename T>
259 bool exists(const std::string& id) const
260 {
261 const auto it = m_objectMap.find(id);
262 if (it == m_objectMap.end()) {
263 return false;
264 }
265 return detail::matchesTypeName<T>(it->second.typeName);
266 }
267
271 std::optional<part::Beam> tryGetBeam(const sequence::Event& event) const
272 {
273 if (const auto& eventId = event.id()) {
274 const auto it = m_eventsInBeams.find(eventId.value());
275 if (it != m_eventsInBeams.end()) {
276 MNX_ASSERT_IF(it->second.location.typeName != part::Beam::JsonSchemaTypeName) {
277 mapping_error err(
278 "The beam mapping for eventId " + formatKeyString(eventId.value())
279 + " was mapped to an object of type \"" + std::string(it->second.location.typeName) + "\"."
280 );
281 if (m_errorHandler) {
282 m_errorHandler.value()(err.what(), event);
283 }
284 throw err;
285 }
286 return part::Beam(root(), it->second.location.location);
287 }
288 }
289 return std::nullopt;
290 }
291
293 [[nodiscard]] std::optional<int> tryGetBeamStartLevel(const std::string& eventId) const
294 {
295 const auto it = m_eventsInBeams.find(eventId);
296 if (it == m_eventsInBeams.end()) {
297 return std::nullopt;
298 }
299 return it->second.startLevel;
300 }
301
303 [[nodiscard]] int getBeamStartLevel(const std::string& eventId) const
304 {
305 if (const auto level = tryGetBeamStartLevel(eventId)) {
306 return *level;
307 }
308 return 0;
309 }
310
312 void clear()
313 {
314 m_objectMap.clear();
315 m_eventsInBeams.clear();
316 m_eventOttavaShift.clear();
317 m_lyricLineOrder.clear();
318 }
319
321 [[nodiscard]] std::optional<int> tryGetOttavaShift(const sequence::Event& event) const
322 {
323 const auto it = m_eventOttavaShift.find(event.pointer().to_string());
324 if (it == m_eventOttavaShift.end()) {
325 return std::nullopt;
326 }
327 return -it->second;
328 }
329
331 [[nodiscard]] int getOttavaShift(const sequence::Event& event) const
332 {
333 if (auto shift = tryGetOttavaShift(event)) {
334 return shift.value();
335 }
336 return 0;
337 }
338
340 [[nodiscard]] const std::vector<std::string>& getLyricLineOrder() const
341 { return m_lyricLineOrder; }
342
343private:
344 std::weak_ptr<json> m_root;
345 std::optional<ErrorHandler> m_errorHandler;
346
347 std::shared_ptr<json> root() const
348 {
349 std::shared_ptr<json> result = m_root.lock();
350 if (!result) {
351 throw std::runtime_error("ID mapping is invalid because the document was destroyed.");
352 }
353 return result;
354 }
355
356 using MappedLocation = struct
357 {
358 json_pointer location;
359 std::string_view typeName;
360 };
361 std::unordered_map<std::string, MappedLocation> m_objectMap;
362 struct BeamMappingEntry
363 {
364 MappedLocation location;
365 int startLevel{0};
366 };
367 std::unordered_map<std::string, BeamMappingEntry> m_eventsInBeams;
368 std::unordered_map<std::string, int> m_eventOttavaShift;
369 std::vector<std::string> m_lyricLineOrder;
370
371 static std::string formatKeyString(const std::string& key) {
372 return "\"" + key + "\"";
373 }
374
375};
376
377} // namespace mnx::util
json_pointer pointer() const
Returns the json_pointer for this node.
Definition BaseTypes.h:260
Represents an MNX document and provides methods for loading and saving.
Definition Document.h:103
Contains information about each level of bea.
Definition Part.h:40
static constexpr std::string_view JsonSchemaTypeName
required for mapping
Definition Part.h:61
Represents a musical event within a sequence.
Definition Sequence.h:418
Provides type-safe ID-based lookup for elements in an MNX document.
Definition EntityMap.h:109
bool exists(const std::string &id) const
Returns whether the specified ID exists in the mapping with type T.
Definition EntityMap.h:259
std::optional< part::Beam > tryGetBeam(const sequence::Event &event) const
Get the beam for an event, if it is mapped.
Definition EntityMap.h:271
std::optional< int > tryGetOttavaShift(const sequence::Event &event) const
Retrieve the ottava shift for an event (if known).
Definition EntityMap.h:321
std::optional< int > tryGetBeamStartLevel(const std::string &eventId) const
Return the secondary beam depth that starts at an event ID, if any.
Definition EntityMap.h:293
int getBeamStartLevel(const std::string &eventId) const
Return the beam start level for an event ID or 0 if none.
Definition EntityMap.h:303
const std::vector< std::string > & getLyricLineOrder() const
Retrieve the lyric line order.
Definition EntityMap.h:340
EntityMap(std::weak_ptr< json > documentRoot, const std::optional< ErrorHandler > &errorHandler=std::nullopt)
Constructs the index for a given document.
Definition EntityMap.h:176
size_t getIndexOf(const std::string &id, const std::optional< Base > &errorLocation=std::nullopt) const
Returns the array index of an object identified by ID.
Definition EntityMap.h:247
void clear()
Clears all mapped items.
Definition EntityMap.h:312
std::optional< T > tryGet(const std::string &id, const std::optional< Base > &errorLocation=std::nullopt) const
Attempts to look up an object by string ID.
Definition EntityMap.h:193
int getOttavaShift(const sequence::Event &event) const
Retrieve the ottava shift for an event. Returns 0 if not cached.
Definition EntityMap.h:331
T get(const std::string &id, const std::optional< Base > &errorLocation=std::nullopt) const
Looks up an object by string ID.
Definition EntityMap.h:223
base class for mapping error exceptions
Definition EntityMap.h:97
json::json_pointer json_pointer
JSON pointer class for MNX.
Definition BaseTypes.h:68