Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public void Load(SpeckleContainerBuilder builder)
builder.AddScoped<SendOperation<MapMember>>();
builder.AddScoped<ArcGISRootObjectBuilder>();
builder.AddScoped<IRootObjectBuilder<MapMember>, ArcGISRootObjectBuilder>();
builder.AddSingleton<ArcGISColorManager>();
builder.AddScoped<ArcGISColorManager>();

builder.AddScoped<ILocalToGlobalUnpacker, LocalToGlobalUnpacker>();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
using System.Drawing;
using ArcGIS.Core.CIM;
using ArcGIS.Core.Data;
using ArcGIS.Desktop.Framework.Threading.Tasks;
using ArcGIS.Desktop.Mapping;
using Speckle.Converters.ArcGIS3.Utils;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.GraphTraversal;
using Speckle.Sdk.Models.Proxies;

namespace Speckle.Connectors.ArcGIS.HostApp;

public class ArcGISColorManager
{
private Dictionary<string, ColorProxy> ColorProxies { get; set; } = new();
public Dictionary<string, Color> ObjectColorsIdMap { get; set; } = new();

/// <summary>
/// Iterates through a given set of arcGIS map members (layers containing objects) and collects their colors.
Expand Down Expand Up @@ -43,6 +48,150 @@ public List<ColorProxy> UnpackColors(List<(MapMember, int)> mapMembersWithDispla
return ColorProxies.Values.ToList();
}

/// <summary>
/// Parse Color Proxies and stores in ObjectColorsIdMap the relationship between object ids and colors
/// </summary>
/// <param name="colorProxies"></param>
/// <param name="onOperationProgressed"></param>
public void ParseColors(List<ColorProxy> colorProxies, Action<string, double?>? onOperationProgressed)
{
// injected as Singleton, so we need to clean existing proxies first
ObjectColorsIdMap = new();
var count = 0;
foreach (ColorProxy colorProxy in colorProxies)
{
onOperationProgressed?.Invoke("Converting colors", (double)++count / colorProxies.Count);
foreach (string objectId in colorProxy.objects)
{
Color convertedColor = Color.FromArgb(colorProxy.value);
ObjectColorsIdMap.TryAdd(objectId, convertedColor);
}
}
}

/// <summary>
/// Create a new CIMUniqueValueClass for UniqueRenderer per each object ID
/// </summary>
/// <param name="tc"></param>
/// <param name="speckleGeometryType"></param>
private CIMUniqueValueClass CreateColorCategory(TraversalContext tc, esriGeometryType speckleGeometryType)
{
Base baseObj = tc.Current;

// declare default white color
Color color = Color.FromArgb(255, 255, 255, 255);

// get color moving upwards from the object
foreach (var parent in tc.GetAscendants())
{
if (parent.applicationId is string appId && ObjectColorsIdMap.TryGetValue(appId, out Color objColor))
{
color = objColor;
break;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we get the color of the closest parent, or the highest in the hierarchy?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand the question here: if the atomic object doesn't have a color, then the color of the closest parent with a color should be used

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's resolve this POC comment before merge

}

CIMSymbolReference symbol = CreateSymbol(speckleGeometryType, color);

// First create a "CIMUniqueValueClass"
List<CIMUniqueValue> listUniqueValues = new() { new CIMUniqueValue { FieldValues = new string[] { baseObj.id } } };

CIMUniqueValueClass newUniqueValueClass =
new()
{
Editable = true,
Label = baseObj.id,
Patch = PatchShape.Default,
Symbol = symbol,
Visible = true,
Values = listUniqueValues.ToArray()
};
return newUniqueValueClass;
}

/// <summary>
/// Create a Symbol from GeometryType and Color
/// </summary>
/// <param name="speckleGeometryType"></param>
/// <param name="color"></param>
private CIMSymbolReference CreateSymbol(esriGeometryType speckleGeometryType, Color color)
{
var symbol = SymbolFactory
.Instance.ConstructPointSymbol(ColorFactory.Instance.CreateColor(color))
.MakeSymbolReference();

switch (speckleGeometryType)
{
case esriGeometryType.esriGeometryLine:
case esriGeometryType.esriGeometryPolyline:
symbol = SymbolFactory
.Instance.ConstructLineSymbol(ColorFactory.Instance.CreateColor(color))
.MakeSymbolReference();
break;
case esriGeometryType.esriGeometryPolygon:
case esriGeometryType.esriGeometryMultiPatch:
symbol = SymbolFactory
.Instance.ConstructPolygonSymbol(ColorFactory.Instance.CreateColor(color))
.MakeSymbolReference();
break;
}

return symbol;
}

/// <summary>
/// Add CIMUniqueValueClass to Layer Renderer (if exists); apply Renderer to Layer (again)
/// </summary>
/// <param name="tc"></param>
/// <param name="trackerItem"></param>
public async Task SetOrEditLayerRenderer(TraversalContext tc, ObjectConversionTracker trackerItem)
{
if (trackerItem.HostAppMapMember is not FeatureLayer fLayer)
{
// do nothing with non-feature layers
return;
}

// declare default grey color, create default symbol for the given layer geometry type
var color = Color.FromArgb(ColorFactory.Instance.GreyRGB.CIMColorToInt());
CIMSymbolReference defaultSymbol = CreateSymbol(fLayer.ShapeType, color);

// get existing renderer classes
List<CIMUniqueValueClass> listUniqueValueClasses = new() { };
var existingRenderer = QueuedTask.Run(() => fLayer.GetRenderer()).Result;
// should be always UniqueRenderer, it's the only type we are creating atm
if (existingRenderer is CIMUniqueValueRenderer uniqueRenderer)
{
if (uniqueRenderer.Groups[0].Classes != null)
{
listUniqueValueClasses.AddRange(uniqueRenderer.Groups[0].Classes.ToList());
}
}

// Add new CIMUniqueValueClass
CIMUniqueValueClass newUniqueValueClass = CreateColorCategory(tc, fLayer.ShapeType);
if (!listUniqueValueClasses.Select(x => x.Label).Contains(newUniqueValueClass.Label))
{
listUniqueValueClasses.Add(newUniqueValueClass);
}
// Create a list of CIMUniqueValueGroup
CIMUniqueValueGroup uvg = new() { Classes = listUniqueValueClasses.ToArray(), };
List<CIMUniqueValueGroup> listUniqueValueGroups = new() { uvg };
// Create the CIMUniqueValueRenderer
CIMUniqueValueRenderer uvr =
new()
{
UseDefaultSymbol = true,
DefaultLabel = "all other values",
DefaultSymbol = defaultSymbol,
Groups = listUniqueValueGroups.ToArray(),
Fields = new string[] { "Speckle_ID" }
};

// Set the feature layer's renderer.
await QueuedTask.Run(() => fLayer.SetRenderer(uvr)).ConfigureAwait(false);
}

private string GetColorApplicationId(int argb, double order) => $"{argb}_{order}";

// Adds the element id to the color proxy based on colorId if it exists in ColorProxies,
Expand Down Expand Up @@ -77,6 +226,11 @@ private void ProcessRasterLayerColors(RasterLayer rasterLayer, int displayPriori
AddElementIdToColorProxy(elementAppId, argb, colorId, displayPriority);
}

/// <summary>
/// Record colors from every feature of the layer into ColorProxies
/// </summary>
/// <param name="layer"></param>
/// <param name="displayPriority"></param>
private void ProcessFeatureLayerColors(FeatureLayer layer, int displayPriority)
{
// first get a list of layer fields
Expand Down Expand Up @@ -247,6 +401,11 @@ out int color
return true;
}

/// <summary>
/// Make comparable the Label string of a UniqueValueRenderer (groupValue), and a Feature Attribute value (rowValue)
/// </summary>
/// <param name="rowValue"></param>
/// <param name="groupValue"></param>
private (string, string) MakeValuesComparable(object? rowValue, string groupValue)
{
string newGroupValue = groupValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using ArcGIS.Core.Geometry;
using ArcGIS.Desktop.Framework.Threading.Tasks;
using ArcGIS.Desktop.Mapping;
using Speckle.Connectors.ArcGIS.HostApp;
using Speckle.Connectors.ArcGIS.Utils;
using Speckle.Connectors.Utils.Builders;
using Speckle.Connectors.Utils.Conversion;
Expand All @@ -16,6 +17,7 @@
using Speckle.Sdk.Models.Collections;
using Speckle.Sdk.Models.GraphTraversal;
using Speckle.Sdk.Models.Instances;
using Speckle.Sdk.Models.Proxies;
using RasterLayer = Speckle.Objects.GIS.RasterLayer;

namespace Speckle.Connectors.ArcGIS.Operations.Receive;
Expand All @@ -30,14 +32,16 @@ public class ArcGISHostObjectBuilder : IHostObjectBuilder
// POC: figure out the correct scope to only initialize on Receive
private readonly IConversionContextStack<ArcGISDocument, Unit> _contextStack;
private readonly GraphTraversal _traverseFunction;
private readonly ArcGISColorManager _colorManager;

public ArcGISHostObjectBuilder(
IRootToHostConverter converter,
IConversionContextStack<ArcGISDocument, Unit> contextStack,
INonNativeFeaturesUtils nonGisFeaturesUtils,
ILocalToGlobalUnpacker localToGlobalUnpacker,
ILocalToGlobalConverterUtils localToGlobalConverterUtils,
GraphTraversal traverseFunction
GraphTraversal traverseFunction,
ArcGISColorManager colorManager
)
{
_converter = converter;
Expand All @@ -46,6 +50,7 @@ GraphTraversal traverseFunction
_localToGlobalUnpacker = localToGlobalUnpacker;
_localToGlobalConverterUtils = localToGlobalConverterUtils;
_traverseFunction = traverseFunction;
_colorManager = colorManager;
}

public async Task<HostObjectBuilderResult> Build(
Expand All @@ -62,6 +67,13 @@ CancellationToken cancellationToken
// Prompt the UI conversion started. Progress bar will swoosh.
onOperationProgressed?.Invoke("Converting", null);

// get colors
List<ColorProxy>? colors = (rootObject["colorProxies"] as List<object>)?.Cast<ColorProxy>().ToList();
if (colors != null)
{
_colorManager.ParseColors(colors, onOperationProgressed);
}

var objectsToConvertTc = _traverseFunction
.Traverse(rootObject)
.Where(ctx => ctx.Current is not Collection || IsGISType(ctx.Current))
Expand Down Expand Up @@ -161,16 +173,20 @@ await QueuedTask
}
else if (bakedMapMembers.TryGetValue(trackerItem.DatasetId, out MapMember? value))
{
// if the layer already created, just add more features to report, and more color categories
// 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);

// add color category
await _colorManager.SetOrEditLayerRenderer(item.Key, trackerItem).ConfigureAwait(false);
}
else
{
// add layer to Map
// no layer yet, create and add layer to Map
MapMember mapMember = await AddDatasetsToMap(trackerItem, createdLayerGroups, projectName, modelName)
.ConfigureAwait(false);

Expand All @@ -187,6 +203,9 @@ await QueuedTask

// add report item
AddResultsFromTracker(trackerItem, results);

// add color category
await _colorManager.SetOrEditLayerRenderer(item.Key, trackerItem).ConfigureAwait(false);
}
onOperationProgressed?.Invoke("Adding to Map", (double)++bakeCount / conversionTracker.Count);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ public List<FieldDescription> GetFieldsFromSpeckleLayer(VectorLayer target)
{
// get all members by default, but only Dynamic ones from the basic geometry
Dictionary<string, object?> members = new();
members["Speckle_ID"] = baseObj.id; // to use for unique color values

// leave out until we decide which properties to support on Receive
/*
Expand All @@ -119,7 +120,12 @@ public List<FieldDescription> GetFieldsFromSpeckleLayer(VectorLayer target)
foreach (KeyValuePair<string, object?> field in members)
{
// POC: TODO check for the forbidden characters/combinations: https://support.esri.com/en-us/knowledge-base/what-characters-should-not-be-used-in-arcgis-for-field--000005588
Func<Base, object?> function = x => x[field.Key];
string key = field.Key;
if (field.Key == "Speckle_ID")
{
key = "id";
}
Func<Base, object?> function = x => x[key];
TraverseAttributes(field, function, fieldsAndFunctions, fieldAdded);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ public Base TransformObjects(Base atomicObject, List<Matrix4x4> matrix)
);
}

string id = atomicObject.id;
atomicObject = (Base)c;
atomicObject.id = id;

// .TransformTo only transfers typed properties, we need to add back the dynamic ones:
foreach (var prop in atomicObject.GetMembers(DynamicBaseMemberType.Dynamic))
Expand Down
Loading