Files
speckle-sharp-connectors/Connectors/ArcGIS/Speckle.Connectors.ArcGIS3/Operations/Receive/HostObjectBuilder.cs
T
2024-07-04 11:56:34 +01:00

294 lines
10 KiB
C#

using System.Diagnostics.Contracts;
using ArcGIS.Desktop.Mapping;
using Speckle.Connectors.Utils.Builders;
using Speckle.Converters.Common;
using Speckle.Core.Logging;
using Speckle.Core.Models;
using Speckle.Converters.ArcGIS3.Utils;
using ArcGIS.Core.Geometry;
using Objects.GIS;
using Speckle.Connectors.Utils.Conversion;
using Speckle.Core.Models.GraphTraversal;
using Speckle.Converters.ArcGIS3;
using RasterLayer = Objects.GIS.RasterLayer;
using Speckle.Connectors.ArcGIS.Utils;
namespace Speckle.Connectors.ArcGIS.Operations.Receive;
public class ArcGISHostObjectBuilder : IHostObjectBuilder
{
private readonly IRootToHostConverter _converter;
private readonly INonNativeFeaturesUtils _nonGisFeaturesUtils;
// POC: figure out the correct scope to only initialize on Receive
private readonly IConversionContextStack<ArcGISDocument, Unit> _contextStack;
private readonly GraphTraversal _traverseFunction;
public ArcGISHostObjectBuilder(
IRootToHostConverter converter,
IConversionContextStack<ArcGISDocument, Unit> contextStack,
INonNativeFeaturesUtils nonGisFeaturesUtils,
GraphTraversal traverseFunction
)
{
_converter = converter;
_contextStack = contextStack;
_nonGisFeaturesUtils = nonGisFeaturesUtils;
_traverseFunction = traverseFunction;
}
public HostObjectBuilderResult Build(
Base rootObject,
string projectName,
string modelName,
Action<string, double?>? onOperationProgressed,
CancellationToken cancellationToken
)
{
// Prompt the UI conversion started. Progress bar will swoosh.
onOperationProgressed?.Invoke("Converting", null);
var objectsToConvert = _traverseFunction
.Traverse(rootObject)
.Where(ctx => ctx.Current is not Collection || IsGISType(ctx.Current))
.Where(ctx => HasGISParent(ctx) is false)
.ToList();
int allCount = objectsToConvert.Count;
int count = 0;
Dictionary<TraversalContext, ObjectConversionTracker> conversionTracker = new();
// 1. convert everything
List<ReceiveConversionResult> results = new(objectsToConvert.Count);
List<string> bakedObjectIds = new();
foreach (TraversalContext ctx in objectsToConvert)
{
string[] path = GetLayerPath(ctx);
Base obj = ctx.Current;
cancellationToken.ThrowIfCancellationRequested();
try
{
if (IsGISType(obj))
{
string nestedLayerPath = $"{string.Join("\\", path)}";
string datasetId = (string)_converter.Convert(obj);
conversionTracker[ctx] = new ObjectConversionTracker(obj, nestedLayerPath, datasetId);
}
else
{
string nestedLayerPath = $"{string.Join("\\", path)}\\{obj.speckle_type.Split(".")[^1]}";
Geometry converted = (Geometry)_converter.Convert(obj);
conversionTracker[ctx] = new ObjectConversionTracker(obj, nestedLayerPath, converted);
}
}
catch (Exception ex) when (!ex.IsFatal()) // DO NOT CATCH SPECIFIC STUFF, conversion errors should be recoverable
{
results.Add(new(Status.ERROR, obj, null, null, ex));
}
onOperationProgressed?.Invoke("Converting", (double)++count / allCount);
}
// 2. convert Database entries with non-GIS geometry datasets
onOperationProgressed?.Invoke("Writing to Database", null);
_nonGisFeaturesUtils.WriteGeometriesToDatasets(conversionTracker);
// Create main group layer
Dictionary<string, GroupLayer> createdLayerGroups = new();
Map map = _contextStack.Current.Document.Map;
GroupLayer groupLayer = LayerFactory.Instance.CreateGroupLayer(map, 0, $"{projectName}: {modelName}");
createdLayerGroups["Basic Speckle Group"] = groupLayer; // key doesn't really matter here
// 3. add layer and tables to the Table Of Content
int bakeCount = 0;
Dictionary<string, MapMember> bakedMapMembers = new();
onOperationProgressed?.Invoke("Adding to Map", bakeCount);
foreach (var item in conversionTracker)
{
cancellationToken.ThrowIfCancellationRequested();
var trackerItem = conversionTracker[item.Key]; // updated tracker object
// BAKE OBJECTS HERE
if (trackerItem.Exception != null)
{
results.Add(new(Status.ERROR, trackerItem.Base, null, null, trackerItem.Exception));
}
else if (trackerItem.DatasetId == null)
{
results.Add(
new(
Status.ERROR,
trackerItem.Base,
null,
null,
new ArgumentException($"Unknown error: Dataset not created for {trackerItem.Base.speckle_type}")
)
);
}
else if (bakedMapMembers.TryGetValue(trackerItem.DatasetId, out MapMember? value))
{
// add layer and layer URI to tracker
trackerItem.AddConvertedMapMember(value);
trackerItem.AddLayerURI(value.URI);
conversionTracker[item.Key] = trackerItem; // not necessary atm, but needed if we use conversionTracker further
// only add a report item
AddResultsFromTracker(trackerItem, results);
}
else
{
// add layer to Map
MapMember mapMember = AddDatasetsToMap(trackerItem, createdLayerGroups);
// add layer and layer URI to tracker
trackerItem.AddConvertedMapMember(mapMember);
trackerItem.AddLayerURI(mapMember.URI);
conversionTracker[item.Key] = trackerItem; // not necessary atm, but needed if we use conversionTracker further
// add layer URI to bakedIds
bakedObjectIds.Add(trackerItem.MappedLayerURI == null ? "" : trackerItem.MappedLayerURI);
// mark dataset as already created
bakedMapMembers[trackerItem.DatasetId] = mapMember;
// add report item
AddResultsFromTracker(trackerItem, results);
}
onOperationProgressed?.Invoke("Adding to Map", (double)++bakeCount / conversionTracker.Count);
}
bakedObjectIds.AddRange(createdLayerGroups.Values.Select(x => x.URI));
// TODO: validated a correct set regarding bakedobject ids
return new(bakedObjectIds, results);
}
private void AddResultsFromTracker(ObjectConversionTracker trackerItem, List<ReceiveConversionResult> results)
{
if (trackerItem.MappedLayerURI == null) // should not happen
{
results.Add(
new(
Status.ERROR,
trackerItem.Base,
null,
null,
new ArgumentException($"Created Layer URI not found for {trackerItem.Base.speckle_type}")
)
);
}
else
{
// encode layer ID and ID of its feature in 1 object represented as string
ObjectID objectId = new(trackerItem.MappedLayerURI, trackerItem.DatasetRow);
if (trackerItem.HostAppGeom != null) // individual hostAppGeometry
{
results.Add(
new(
Status.SUCCESS,
trackerItem.Base,
objectId.ObjectIdToString(),
trackerItem.HostAppGeom.GetType().ToString()
)
);
}
else // hostApp Layers
{
results.Add(
new(
Status.SUCCESS,
trackerItem.Base,
objectId.ObjectIdToString(),
trackerItem.HostAppMapMember?.GetType().ToString()
)
);
}
}
}
private MapMember AddDatasetsToMap(
ObjectConversionTracker trackerItem,
Dictionary<string, GroupLayer> createdLayerGroups
)
{
// get layer details
string? datasetId = trackerItem.DatasetId; // should not ne null here
Uri uri = new($"{_contextStack.Current.Document.SpeckleDatabasePath.AbsolutePath.Replace('/', '\\')}\\{datasetId}");
string nestedLayerName = trackerItem.NestedLayerName;
// add group for the current layer
string shortName = nestedLayerName.Split("\\")[^1];
string nestedLayerPath = string.Join("\\", nestedLayerName.Split("\\").SkipLast(1));
GroupLayer groupLayer = CreateNestedGroupLayer(nestedLayerPath, createdLayerGroups);
// Most of the Speckle-written datasets will be containing geometry and added as Layers
// although, some datasets might be just tables (e.g. native GIS Tables, in the future maybe Revit schedules etc.
// We can create a connection to the dataset in advance and determine its type, but this will be more
// expensive, than assuming by default that it's a layer with geometry (which in most cases it's expected to be)
try
{
var layer = LayerFactory.Instance.CreateLayer(uri, groupLayer, layerName: shortName);
layer.SetExpanded(true);
return layer;
}
catch (ArgumentException)
{
var table = StandaloneTableFactory.Instance.CreateStandaloneTable(uri, groupLayer, tableName: shortName);
return table;
}
}
private GroupLayer CreateNestedGroupLayer(string nestedLayerPath, Dictionary<string, GroupLayer> createdLayerGroups)
{
GroupLayer lastGroup = createdLayerGroups.FirstOrDefault().Value;
if (lastGroup == null) // if layer not found
{
throw new InvalidOperationException("Speckle Layer Group not found");
}
// iterate through each nested level
string createdGroupPath = "";
var allPathElements = nestedLayerPath.Split("\\").Where(x => !string.IsNullOrEmpty(x));
foreach (string pathElement in allPathElements)
{
createdGroupPath += "\\" + pathElement;
if (createdLayerGroups.TryGetValue(createdGroupPath, out var existingGroupLayer))
{
lastGroup = existingGroupLayer;
}
else
{
// create new GroupLayer under last found Group, named with last pathElement
lastGroup = LayerFactory.Instance.CreateGroupLayer(lastGroup, 0, pathElement);
lastGroup.SetExpanded(true);
}
createdLayerGroups[createdGroupPath] = lastGroup;
}
return lastGroup;
}
[Pure]
private static string[] GetLayerPath(TraversalContext context)
{
string[] collectionBasedPath = context.GetAscendantOfType<Collection>().Select(c => c.name).ToArray();
string[] reverseOrderPath =
collectionBasedPath.Length != 0 ? collectionBasedPath : context.GetPropertyPath().ToArray();
var originalPath = reverseOrderPath.Reverse().ToArray();
return originalPath.Where(x => !string.IsNullOrEmpty(x)).ToArray();
}
[Pure]
private static bool HasGISParent(TraversalContext context)
{
List<Base> gisLayers = context.GetAscendants().Where(IsGISType).Where(obj => obj != context.Current).ToList();
return gisLayers.Count > 0;
}
[Pure]
private static bool IsGISType(Base obj)
{
return obj is RasterLayer or VectorLayer;
}
}