Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add type-mapping to data objects using the WCF DataContract attribute. #264

Merged
merged 1 commit into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Assets/Scripts/Layers/LayerTypes/FolderLayer.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using System;
using System.Runtime.Serialization;
using Netherlands3D.Twin.Projects;

namespace Netherlands3D.Twin.Layers
{
[Serializable]
[DataContract(Namespace = "https://netherlands3d.eu/schemas/projects/layers", Name = "Folder")]
public class FolderLayer : LayerData
{
public FolderLayer(string name) : base(name)
Expand Down
13 changes: 7 additions & 6 deletions Assets/Scripts/Layers/LayerTypes/LayerData.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using Netherlands3D.Twin.Layers.Properties;
using Netherlands3D.Twin.Projects;
using Newtonsoft.Json;
Expand All @@ -12,13 +13,13 @@ namespace Netherlands3D.Twin.Layers
[Serializable]
public class LayerData
{
[SerializeField, JsonProperty] protected Guid UUID = Guid.NewGuid();
[SerializeField, JsonProperty] protected string name;
[SerializeField, JsonProperty] protected bool activeSelf = true;
[SerializeField, JsonProperty] protected Color color = new Color(86f / 256f, 160f / 256f, 227f / 255f);
[SerializeField, JsonProperty] protected List<LayerData> children = new();
[SerializeField, DataMember] protected Guid UUID = Guid.NewGuid();
[SerializeField, DataMember] protected string name;
[SerializeField, DataMember] protected bool activeSelf = true;
[SerializeField, DataMember] protected Color color = new Color(86f / 256f, 160f / 256f, 227f / 255f);
[SerializeField, DataMember] protected List<LayerData> children = new();
[JsonIgnore] protected LayerData parent; //not serialized to avoid a circular reference
[SerializeField, JsonProperty] protected List<LayerPropertyData> layerProperties = new();
[SerializeField, DataMember] protected List<LayerPropertyData> layerProperties = new();
[JsonIgnore] public RootLayer Root => ProjectData.Current.RootLayer;
[JsonIgnore] public LayerData ParentLayer => parent;

Expand Down
10 changes: 5 additions & 5 deletions Assets/Scripts/Layers/LayerTypes/PolygonSelectionLayer.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using Netherlands3D.Coordinates;
using Netherlands3D.SelectionTools;
using Netherlands3D.Twin.FloatingOrigin;
using Netherlands3D.Twin.Layers.Properties;
using Netherlands3D.Twin.Projects;
using Newtonsoft.Json;
using UnityEngine;
using UnityEngine.Events;
Expand All @@ -19,11 +18,12 @@ public enum ShapeType
Line = 2
}

[Serializable]
[DataContract(Namespace = "https://netherlands3d.eu/schemas/projects/layers", Name = "PolygonSelection")]
public class PolygonSelectionLayer : ReferencedLayerData, ILayerWithPropertyData//, ILayerWithPropertyPanels
{
[JsonProperty] public List<Coordinate> OriginalPolygon { get; private set; }
[SerializeField, JsonProperty] private ShapeType shapeType;
[DataMember] public List<Coordinate> OriginalPolygon { get; private set; }
[DataMember] private ShapeType shapeType;

[JsonIgnore] private PolygonSelectionLayerPropertyData polygonPropertyData;
[JsonIgnore] public LayerPropertyData PropertyData => polygonPropertyData;
[JsonIgnore] public CompoundPolygon Polygon { get; set; }
Expand Down
5 changes: 3 additions & 2 deletions Assets/Scripts/Layers/LayerTypes/ReferencedLayerData.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using Netherlands3D.Twin.Layers.Properties;
using Netherlands3D.Twin.Projects;
using Newtonsoft.Json;
using UnityEngine;

namespace Netherlands3D.Twin.Layers
{
[Serializable]
[DataContract(Namespace = "https://netherlands3d.eu/schemas/projects/layers", Name = "Prefab")]
public class ReferencedLayerData : LayerData
{
[SerializeField, JsonProperty] private string prefabId;
[DataMember] private string prefabId;
[JsonIgnore] public LayerGameObject Reference { get; }
[JsonIgnore] public bool KeepReferenceOnDestroy { get; set; } = false;

Expand Down
3 changes: 2 additions & 1 deletion Assets/Scripts/Layers/LayerTypes/RootLayer.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using Netherlands3D.Twin.Projects;
using Newtonsoft.Json;
using UnityEngine;

namespace Netherlands3D.Twin.Layers
{
[Serializable]
[DataContract(Namespace = "https://netherlands3d.eu/schemas/projects/layers", Name = "Root")]
public class RootLayer : LayerData
{
[JsonIgnore] public List<LayerData> SelectedLayers { get; private set; } = new();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using Newtonsoft.Json;
using UnityEngine;
using UnityEngine.Events;

namespace Netherlands3D.Twin.Layers.Properties
{
[Serializable]
[DataContract(Namespace = "https://netherlands3d.eu/schemas/projects/layers/properties", Name = "CartesianTileSubObjectColor")]
public class CartesianTileSubObjectColorPropertyData : LayerPropertyData, ILayerPropertyDataWithAssets
{
[SerializeField, JsonProperty] private Uri data;
[DataMember] private Uri data;

[JsonIgnore] public readonly UnityEvent<Uri> OnDataChanged = new();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Runtime.Serialization;
using Newtonsoft.Json;
using UnityEngine;

Expand All @@ -11,6 +12,6 @@ public class LayerPropertyData
/// Property data has a unique identifier for tracking which data belongs to this
/// property; such as assets.
/// </summary>
[SerializeField, JsonProperty] public Guid UUID = Guid.NewGuid();
[DataMember] public Guid UUID = Guid.NewGuid();
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.Serialization;
using Newtonsoft.Json;

namespace Netherlands3D.Twin.Layers.Properties
{
[Serializable]
[DataContract(Namespace = "https://netherlands3d.eu/schemas/projects/layers/properties", Name = "Url")]
public class LayerURLPropertyData : LayerPropertyData, ILayerPropertyDataWithAssets
{
public string url = "";
[DataMember] public string url = "";

public IEnumerable<LayerAsset> GetAssets()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
using System.Collections;
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using UnityEngine;
using Newtonsoft.Json;
using UnityEngine.Events;

namespace Netherlands3D.Twin.Layers.Properties
{
[DataContract(Namespace = "https://netherlands3d.eu/schemas/projects/layers/properties", Name = "Obj")]
public class ObjPropertyData : LayerPropertyData, ILayerPropertyDataWithAssets
{
[SerializeField, JsonProperty] private Uri objFile;
[DataMember] private Uri objFile;

[JsonIgnore] public readonly UnityEvent<Uri> OnDataChanged = new();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization;
using Newtonsoft.Json;
using UnityEngine;
using UnityEngine.Events;

namespace Netherlands3D.Twin.Layers.Properties
{
[DataContract(Namespace = "https://netherlands3d.eu/schemas/projects/layers/properties", Name = "PolygonSelection")]
public class PolygonSelectionLayerPropertyData : LayerPropertyData
{
[SerializeField, JsonProperty] private float lineWidth = 10f;
[SerializeField, JsonProperty] private float extrusionHeight = 10f;
[DataMember] private float lineWidth = 10f;
[DataMember] private float extrusionHeight = 10f;

[JsonIgnore] public readonly UnityEvent<float> OnLineWidthChanged = new();
[JsonIgnore] public readonly UnityEvent<float> OnExtrusionHeightChanged = new();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
using System;
using System.Runtime.Serialization;
using Newtonsoft.Json;
using UnityEngine;
using UnityEngine.Events;

namespace Netherlands3D.Twin.Layers.Properties
{
[Serializable]
[DataContract(Namespace = "https://netherlands3d.eu/schemas/projects/layers/properties", Name = "3DTiles")]
public class Tile3DLayerPropertyData : LayerPropertyData
{
[SerializeField, JsonProperty] private string url;
[DataMember] private string url;

[JsonIgnore]
public string Url
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization;
using Netherlands3D.Coordinates;
using Newtonsoft.Json;
using UnityEngine;
using UnityEngine.Events;

namespace Netherlands3D.Twin.Layers.Properties
{
[Serializable]
[DataContract(Namespace = "https://netherlands3d.eu/schemas/projects/layers/properties", Name = "Transform")]
public class TransformLayerPropertyData : LayerPropertyData
{
[SerializeField, JsonProperty] private Coordinate position;
[SerializeField, JsonProperty] private Vector3 eulerRotation;
[SerializeField, JsonProperty] private Vector3 localScale;
[DataMember] private Coordinate position;
[DataMember] private Vector3 eulerRotation;
[DataMember] private Vector3 localScale;

[JsonIgnore]
public Coordinate Position
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
using System;
using System.Runtime.Serialization;
using Newtonsoft.Json;
using UnityEngine;
using UnityEngine.Events;

namespace Netherlands3D.Twin.Layers.Properties
{
[Serializable]
[DataContract(Namespace = "https://netherlands3d.eu/schemas/projects/layers/properties", Name = "Windmill")]
public class WindmillPropertyData : LayerPropertyData
{
[SerializeField, JsonProperty] private float axisHeight = 120f;
[SerializeField, JsonProperty] private float rotorDiameter = 120f;
[DataMember] private float axisHeight = 120f;
[DataMember] private float rotorDiameter = 120f;

[JsonIgnore] public readonly UnityEvent<float> OnRotorDiameterChanged = new();
[JsonIgnore] public readonly UnityEvent<float> OnAxisHeightChanged = new();
Expand Down
109 changes: 109 additions & 0 deletions Assets/Scripts/Projects/DataContractSerializationBinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using Newtonsoft.Json.Serialization;

namespace Netherlands3D.Twin.Projects
{
/// <summary>
/// For Netherlands3D, we want to make sure that changes in the structure of the code won't affect how types
/// are linked in the serialized file. By default, JSON.net will include a type reference using the Assembly name
/// and Class name (including namespace) in the serialized JSON; but this will break if we were to move classes to
/// another assembly (such as packaging a building block or functionality) or when we need to change the namespace
/// of a class during refactoring
///
/// This Serialization Binder will ensure that if a DataContract attribute is present with a Data object -which is
/// recommended for Netherlands3D objects- that the name and namespace with that attribute is used to populate the
/// `$type` field in the JSON output. This will detach the name of the data object with the name of the class and
/// give more freedom to change the internals of a building block or functionanality.
///
/// This serialization binder will act as a decorator (https://refactoring.guru/design-patterns/decorator) around
/// another SerializationBinder. When there is no DataContract attribute defined, the decorated Serialization Binder
/// is invoked. Generally you provide the DefaultSerializationBinder by Newtonsoft, so that regular classes (such
/// as the Unity color, Vector3 or others) still correctly serialize.
/// </summary>
public class DataContractSerializationBinder: ISerializationBinder
{
private readonly ISerializationBinder decoratedSerializationBinder;
private IDictionary<string, Type> KnownTypes { get; set; } = new Dictionary<string, Type>();

public DataContractSerializationBinder(ISerializationBinder decoratedSerializationBinder)
{
this.decoratedSerializationBinder = decoratedSerializationBinder;
IndexAliasesForWellDefinedDataObjects();
}

public Type BindToType(string assemblyName, string typeName)
{
var typeCodeAndType = KnownTypes.FirstOrDefault(t => t.Key == typeName);
if (typeCodeAndType.Equals(default(KeyValuePair<string, Type>)))
{
return decoratedSerializationBinder.BindToType(assemblyName, typeName);
}

return typeCodeAndType.Value;
}

public void BindToName(Type serializedType, out string assemblyName, out string typeName)
{
var typeCodeAndType = KnownTypes.FirstOrDefault(kv => kv.Value == serializedType);
if (typeCodeAndType.Equals(default(KeyValuePair<string, Type>)))
{
decoratedSerializationBinder.BindToName(serializedType, out assemblyName, out typeName);
return;
}

var typeCode = typeCodeAndType.Key;

assemblyName = null;
typeName = typeCode;
}

private void IndexAliasesForWellDefinedDataObjects()
{
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in assemblies)
{
IndexAllTypesWithADataContract(assembly);
}
}

private void IndexAllTypesWithADataContract(Assembly assembly)
{
var types = assembly.GetTypes().Where(t => t.IsDefined(typeof(DataContractAttribute)));
foreach (var type in types)
{
IndexTypeWithDataContract(type);
}
}

private void IndexTypeWithDataContract(Type type)
{
var attribute = Attribute.GetCustomAttribute(type, typeof(DataContractAttribute)) as DataContractAttribute;
if (attribute == null) return;

KnownTypes.TryAdd(ExtractTypeAlias(type, attribute), type);
}

private static string ExtractTypeAlias(Type type, DataContractAttribute attribute)
{
// By default, we assume DataContract doesn't have additional info defined and we use the Type's
// full name as a type code
string alias = type.FullName ?? type.Name;

// If there is no name associated with the DataContract, that's OK and we return the type's name.
if (string.IsNullOrEmpty(attribute.Name)) return alias;

// if DataContract does have a name defined; we use that so that we type-map the data and prevent future
// issues when changing the location or namespace of a serialized class.
alias = attribute.Name;

if (string.IsNullOrEmpty(attribute.Namespace)) return alias;

// It would be even better if the DataContract has a vendor specific namespace, to prevent naming clashes
return $"{attribute.Namespace}/{alias}";
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Assets/Scripts/Projects/ProjectDataStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using JetBrains.Annotations;
using Netherlands3D.Twin.Layers;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using UnityEditor;
using UnityEngine;

Expand All @@ -25,7 +26,8 @@ public class ProjectDataStore : ScriptableObject
private readonly JsonSerializerSettings serializerSettings = new()
{
TypeNameHandling = TypeNameHandling.Auto,
Formatting = Formatting.Indented
Formatting = Formatting.Indented,
SerializationBinder = new DataContractSerializationBinder(new DefaultSerializationBinder())
};

[SerializeField] private string DefaultFileName = "NL3D_Project_";
Expand Down
Loading