Skip to content

Commit

Permalink
Merge pull request #1090 from hypar-io/sharedObjects
Browse files Browse the repository at this point in the history
Optimize model serialization by using SharedObjects for reusable data (#1090)

Co-Authored-By: katehryhorenko <[email protected]>
  • Loading branch information
katehryhorenko and katehryhorenko authored Jan 22, 2025
2 parents 114dbb4 + 5ed2a72 commit 5fd519f
Show file tree
Hide file tree
Showing 4 changed files with 349 additions and 36 deletions.
7 changes: 7 additions & 0 deletions Elements/src/Element.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,13 @@ protected virtual void RaisePropertyChanged([System.Runtime.CompilerServices.Cal
[JsonProperty("Mappings", Required = Required.Default, NullValueHandling = NullValueHandling.Ignore)]
internal Dictionary<string, MappingBase> Mappings { get; set; } = null;

/// <summary>
/// An optional shared object that can be used to share data between elements.
/// </summary>
/// <value></value>
[JsonProperty("SharedObject", Required = Required.Default, NullValueHandling = NullValueHandling.Ignore)]
public SharedObject SharedObject { get; set; }

/// <summary>
/// The method used to set a mapping for a given context.
/// </summary>
Expand Down
119 changes: 88 additions & 31 deletions Elements/src/Model.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
using Elements.Geometry.Solids;
using Elements.GeoJSON;
using System.IO;
using System.Text;

namespace Elements
{
Expand All @@ -21,6 +20,45 @@ namespace Elements
/// </summary>
public class Model
{
private class GatherSubElementsResult
{
/// <summary>
/// List of elements collected from the object.
/// </summary>
public List<Element> Elements { get; } = new List<Element>();
/// <summary>
/// List of shared objects collected from the object.
/// </summary>
public List<SharedObject> SharedObjects { get; } = new List<SharedObject>();
/// <summary>
/// List of elements collected from the shared object's properties.
///
/// If shared object is marked as JsonIgnore (e.g. RepresentationInstance), it will not be
/// serialized to JSON, but its properties will be collected here so they can be used
/// during gltf serialization.
/// </summary>
public List<Element> ElementsFromSharedObjectProperties { get; } = new List<Element>();

public void MergeSubResult(GatherSubElementsResult gatherResult, bool hasJsonIgnore, bool isTypeRelatedToSharedObjects)
{
if (isTypeRelatedToSharedObjects)
{
ElementsFromSharedObjectProperties.AddRange(gatherResult.ElementsFromSharedObjectProperties);
}
else
{
Elements.AddRange(gatherResult.Elements);
}
// do not save shared objects marked with JsonIgnore
if (!hasJsonIgnore)
{
SharedObjects.AddRange(gatherResult.SharedObjects);
Elements.AddRange(gatherResult.ElementsFromSharedObjectProperties);
}
ElementsFromSharedObjectProperties.AddRange(gatherResult.ElementsFromSharedObjectProperties);
}
}

/// <summary>The origin of the model.</summary>
[JsonProperty("Origin", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
[Obsolete("Use Transform instead.")]
Expand All @@ -35,6 +73,10 @@ public class Model
[System.ComponentModel.DataAnnotations.Required]
public System.Collections.Generic.IDictionary<Guid, Element> Elements { get; set; } = new System.Collections.Generic.Dictionary<Guid, Element>();

/// <summary>A collection of SharedObjects keyed by their identifiers.</summary>
[JsonProperty("SharedObjects", Required = Required.Default)]
public System.Collections.Generic.IDictionary<Guid, SharedObject> SharedObjects { get; set; } = new System.Collections.Generic.Dictionary<Guid, SharedObject>();

/// <summary>
/// Collection of subelements from shared objects or RepresentationInstances (e.g. SolidRepresentation.Profile or RepresentationInstance.Material).
/// We do not serialize shared objects to json, but we do include them in other formats like gltf.
Expand Down Expand Up @@ -123,8 +165,8 @@ public void AddElement(Element element, bool gatherSubElements = true, bool upda
// to the elements dictionary first. This will ensure that
// those elements will be read out and be available before
// an attempt is made to deserialize the element itself.
var subElements = RecursiveGatherSubElements(element, out var elementsToIgnore);
foreach (var e in subElements)
var gatherSubElementsResult = RecursiveGatherSubElements(element);
foreach (var e in gatherSubElementsResult.Elements)
{
if (!this.Elements.ContainsKey(e.Id))
{
Expand All @@ -138,7 +180,15 @@ public void AddElement(Element element, bool gatherSubElements = true, bool upda
}
}

foreach (var e in elementsToIgnore)
foreach (var sharedObject in gatherSubElementsResult.SharedObjects)
{
if (!SharedObjects.ContainsKey(sharedObject.Id))
{
SharedObjects.Add(sharedObject.Id, sharedObject);
}
}

foreach (var e in gatherSubElementsResult.ElementsFromSharedObjectProperties)
{
if (!SubElementsFromSharedObjects.ContainsKey(e.Id))
{
Expand Down Expand Up @@ -453,23 +503,21 @@ public static Model FromJson(string json, bool forceTypeReload = false)
return FromJson(json, out _, forceTypeReload);
}

private List<Element> RecursiveGatherSubElements(object obj, out List<Element> elementsToIgnore)
private GatherSubElementsResult RecursiveGatherSubElements(object obj)
{
// A dictionary created for the purpose of caching properties
// that we need to recurse, for types that we've seen before.
var props = new Dictionary<Type, List<PropertyInfo>>();

return RecursiveGatherSubElementsInternal(obj, props, out elementsToIgnore);
return RecursiveGatherSubElementsInternal(obj, props);
}

private List<Element> RecursiveGatherSubElementsInternal(object obj, Dictionary<Type, List<PropertyInfo>> properties, out List<Element> elementsToIgnore)
private GatherSubElementsResult RecursiveGatherSubElementsInternal(object obj, Dictionary<Type, List<PropertyInfo>> properties)
{
var elements = new List<Element>();
elementsToIgnore = new List<Element>();
GatherSubElementsResult result = new GatherSubElementsResult();

if (obj == null)
{
return elements;
return result;
}

var e = obj as Element;
Expand All @@ -478,7 +526,7 @@ private List<Element> RecursiveGatherSubElementsInternal(object obj, Dictionary<
// Do nothing. The Element has already
// been added. This assumes that that the sub-elements
// have been added as well and we don't need to continue.
return elements;
return result;
}

// This explicit loop is because we have mappings marked as internal so it's elements won't be automatically serialized.
Expand All @@ -487,7 +535,17 @@ private List<Element> RecursiveGatherSubElementsInternal(object obj, Dictionary<
foreach (var map in e.Mappings ?? new Dictionary<string, MappingBase>())
{
if (!Elements.ContainsKey(map.Value.Id))
{ elements.Add(map.Value); }
{ result.Elements.Add(map.Value); }
}
}

var sharedObject = obj as SharedObject;
// if this shared object is already in the list, we don't need to process and add it again
if (sharedObject != null)
{
if (SharedObjects.ContainsKey(sharedObject.Id))
{
return result;
}
}

Expand All @@ -498,7 +556,7 @@ private List<Element> RecursiveGatherSubElementsInternal(object obj, Dictionary<
// could be elements.
if (!t.IsClass || t == typeof(string))
{
return elements;
return result;
}

List<PropertyInfo> constrainedProps;
Expand All @@ -515,7 +573,7 @@ private List<Element> RecursiveGatherSubElementsInternal(object obj, Dictionary<
properties.Add(t, constrainedProps);
}

var elementsFromProperties = new List<Element>();
bool isTypeRelatedToSharedObjects = IsTypeRelatedToSharedObjects(t);
foreach (var p in constrainedProps)
{
try
Expand All @@ -526,12 +584,15 @@ private List<Element> RecursiveGatherSubElementsInternal(object obj, Dictionary<
continue;
}

// Do not save shared object to the model if it is marked with JsonIgnore (e.g. ElementRepresentation)
bool hasJsonIgnore = p.GetCustomAttributes(typeof(JsonIgnoreAttribute), true).Any();

if (pValue is IList elems)
{
foreach (var item in elems)
{
elementsFromProperties.AddRange(RecursiveGatherSubElementsInternal(item, properties, out var elementsFromItemToIgnore));
elementsToIgnore.AddRange(elementsFromItemToIgnore);
var subElements = RecursiveGatherSubElementsInternal(item, properties);
result.MergeSubResult(subElements, hasJsonIgnore, isTypeRelatedToSharedObjects);
}
continue;
}
Expand All @@ -541,35 +602,32 @@ private List<Element> RecursiveGatherSubElementsInternal(object obj, Dictionary<
{
foreach (var value in dict.Values)
{
elementsFromProperties.AddRange(RecursiveGatherSubElementsInternal(value, properties, out var elementsFromValueToIgnore));
elementsToIgnore.AddRange(elementsFromValueToIgnore);
var subElements = RecursiveGatherSubElementsInternal(value, properties);
result.MergeSubResult(subElements, hasJsonIgnore, isTypeRelatedToSharedObjects);
}
continue;
}

elementsFromProperties.AddRange(RecursiveGatherSubElementsInternal(pValue, properties, out var elementsFromPropertyToIgnore));
elementsToIgnore.AddRange(elementsFromPropertyToIgnore);
var gatheredSubElements = RecursiveGatherSubElementsInternal(pValue, properties);
result.MergeSubResult(gatheredSubElements, hasJsonIgnore, isTypeRelatedToSharedObjects);
}
catch (Exception ex)
{
throw new Exception($"The {p.Name} property or one of its children was not valid for introspection. Check the inner exception for details.", ex);
}
}
if (IsTypeRelatedToSharedObjects(t))
{
elementsToIgnore.AddRange(elementsFromProperties);
}
else

if (e != null)
{
elements.AddRange(elementsFromProperties);
result.Elements.Add(e);
}

if (e != null)
if (sharedObject != null)
{
elements.Add(e);
result.SharedObjects.Add(sharedObject);
}

return elements;
return result;
}

/// <summary>
Expand Down Expand Up @@ -624,7 +682,6 @@ internal static bool IsValidForRecursiveAddition(Type t)

private static bool IsTypeRelatedToSharedObjects(Type t)
{

return typeof(SharedObject).IsAssignableFrom(t)
|| typeof(RepresentationInstance).IsAssignableFrom(t);
}
Expand Down
Loading

0 comments on commit 5fd519f

Please sign in to comment.