hrntsm/Tunny

View on GitHub
Tunny/Util/GrasshopperInOut.cs

Summary

Maintainability
D
2 days
Test Coverage
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows.Forms;

using GalapagosComponents;

using Grasshopper.GUI.Base;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Data;
using Grasshopper.Kernel.Parameters;
using Grasshopper.Kernel.Special;
using Grasshopper.Kernel.Types;

using Rhino.Geometry;

using Tunny.Component.Params;
using Tunny.Component.Util;
using Tunny.Core.Input;
using Tunny.Core.Util;
using Tunny.Input;
using Tunny.Type;
using Tunny.UI;

namespace Tunny.Util
{
    public class GrasshopperInOut
    {
        private readonly GH_Document _document;
        private readonly List<Guid> _inputGuids;
        private readonly GH_Component _component;
        private List<GalapagosGeneListObject> _genePool;
        private GH_FishAttribute _attributes;
        private List<GH_NumberSlider> _sliders;
        private List<TunnyValueList> _valueLists;

        public Objective Objectives { get; private set; }
        public List<VariableBase> Variables { get; private set; }
        public Artifact Artifacts { get; private set; }
        public Dictionary<string, FishEgg> EnqueueItems { get; private set; }
        public bool HasConstraint { get; private set; }
        public bool IsLoadCorrectly { get; }

        public GrasshopperInOut(GH_Component component, bool getVariableOnly = false)
        {
            TLog.MethodStart();
            _component = component;
            _document = _component.OnPingDocument();
            _inputGuids = new List<Guid>();

            IsLoadCorrectly = getVariableOnly
                ? SetVariables()
                : SetVariables() && SetObjectives() && SetAttributes() && SetArtifacts();
        }

        private bool SetVariables()
        {
            TLog.MethodStart();
            _sliders = new List<GH_NumberSlider>();
            _valueLists = new List<TunnyValueList>();
            _genePool = new List<GalapagosGeneListObject>();

            _inputGuids.AddRange(_component.Params.Input[0].Sources.Select(source => source.InstanceGuid));
            if (_inputGuids.Count == 0)
            {
                NoVariableInputError();
                return false;
            }

            var variables = new List<VariableBase>();
            if (!FilterInputVariables()) { return false; }
            SetInputSliderValues(variables);
            SetInputGenePoolValues(variables);
            SetInputValueItem(variables);
            Variables = variables;
            return true;
        }

        private static void NoVariableInputError()
        {
            TLog.MethodStart();
            TunnyMessageBox.Show(
                "No input variables found. \nPlease connect a number slider to the input of the component.",
                "Tunny",
                MessageBoxButtons.OK,
                MessageBoxIcon.Error);
        }

        private bool FilterInputVariables()
        {
            TLog.MethodStart();
            var errorInputGuids = new List<Guid>();
            foreach ((IGH_DocumentObject docObject, int _) in _inputGuids.Select((guid, i) => (_document.FindObject(guid, false), i)))
            {
                switch (docObject)
                {
                    case GH_NumberSlider slider:
                        _sliders.Add(slider);
                        break;
                    case GalapagosGeneListObject genePool:
                        _genePool.Add(genePool);
                        break;
                    case TunnyValueList valueList:
                        _valueLists.Add(valueList);
                        break;
                    case Param_FishEgg fishEgg:
                        if (fishEgg.VolatileDataCount != 0)
                        {
                            EnqueueItems = ((GH_FishEgg)fishEgg.VolatileData.AllData(true).First()).Value;
                        }
                        break;
                    default:
                        errorInputGuids.Add(docObject.InstanceGuid);
                        break;
                }
            }
            return CheckHasIncorrectVariableInput(errorInputGuids);
        }

        private bool CheckHasIncorrectVariableInput(IReadOnlyCollection<Guid> errorInputGuids)
        {
            TLog.MethodStart();
            return errorInputGuids.Count <= 0 || ShowIncorrectVariableInputMessage(errorInputGuids);
        }

        private bool ShowIncorrectVariableInputMessage(IEnumerable<Guid> errorGuids)
        {
            TLog.MethodStart();
            TunnyMessageBox.Show(
                "Input variables must be either a number slider or a gene pool.\nError input will automatically remove.",
                "Tunny",
                MessageBoxButtons.OK,
                MessageBoxIcon.Error);
            foreach (Guid guid in errorGuids)
            {
                _component.Params.Input[0].RemoveSource(guid);
            }
            _component.ExpireSolution(true);
            return false;
        }

        private void SetInputSliderValues(List<VariableBase> variables)
        {
            TLog.MethodStart();
            int i = 0;

            foreach (GH_NumberSlider slider in _sliders)
            {
                decimal min = slider.Slider.Minimum;
                decimal max = slider.Slider.Maximum;
                decimal value = slider.Slider.Value;
                Guid id = slider.InstanceGuid;

                decimal lowerBond;
                decimal upperBond;
                bool isInteger;
                string nickName = slider.NickName;
                if (nickName == "")
                {
                    nickName = "param" + i++;
                }
                double eps = Convert.ToDouble(slider.Slider.Epsilon);
                switch (slider.Slider.Type)
                {
                    case GH_SliderAccuracy.Even:
                        lowerBond = min / 2;
                        upperBond = max / 2;
                        isInteger = true;
                        break;
                    case GH_SliderAccuracy.Odd:
                        lowerBond = (min - 1) / 2;
                        upperBond = (max - 1) / 2;
                        isInteger = true;
                        break;
                    case GH_SliderAccuracy.Integer:
                        lowerBond = min;
                        upperBond = max;
                        isInteger = true;
                        break;
                    default:
                        lowerBond = min;
                        upperBond = max;
                        isInteger = false;
                        break;
                }

                variables.Add(new NumberVariable(Convert.ToDouble(lowerBond), Convert.ToDouble(upperBond), isInteger, nickName, eps, Convert.ToDouble(value), id));
            }
        }

        private void SetInputGenePoolValues(List<VariableBase> variables)
        {
            TLog.MethodStart();
            var nickNames = new List<string>();
            for (int i = 0; i < _genePool.Count; i++)
            {
                GalapagosGeneListObject genePool = _genePool[i];
                string nickName = nickNames.Contains(genePool.NickName)
                    ? genePool.NickName + i + "-" : genePool.NickName;
                nickNames.Add(nickName);
                bool isInteger = genePool.Decimals == 0;
                decimal lowerBond = genePool.Minimum;
                decimal upperBond = genePool.Maximum;
                double eps = Math.Pow(10, -genePool.Decimals);
                Guid id = genePool.InstanceGuid;
                for (int j = 0; j < genePool.Count; j++)
                {
                    IGH_Goo[] goo = genePool.VolatileData.AllData(false).ToArray();
                    var ghNumber = (GH_Number)goo[j];
                    string name = nickNames[i] + j;
                    variables.Add(new NumberVariable(Convert.ToDouble(lowerBond), Convert.ToDouble(upperBond), isInteger, name, eps, ghNumber.Value, id));
                }
            }
        }

        private void SetInputValueItem(List<VariableBase> variables)
        {
            TLog.MethodStart();
            foreach (TunnyValueList valueList in _valueLists)
            {
                string nickName = valueList.NickName;
                Guid id = valueList.InstanceGuid;
                string[] categories = valueList.ListItems.Select(value => value.Name).ToArray();
                string selectedItem = valueList.FirstSelectedItem.Name;
                variables.Add(new CategoricalVariable(categories, selectedItem, nickName, id));
            }
        }

        private bool SetObjectives()
        {
            TLog.MethodStart();
            if (_component.Params.Input[1].SourceCount == 0)
            {
                return ShowNoObjectiveFoundMessage();
            }
            var unsupportedObjectives = new List<IGH_Param>();
            foreach (IGH_Param param in _component.Params.Input[1].Sources)
            {
                switch (param)
                {
                    case Param_Number _:
                    case Param_FishPrint _:
                        break;
                    default:
                        unsupportedObjectives.Add(param);
                        break;
                }
            }
            if (unsupportedObjectives.Count > 0)
            {
                return ShowIncorrectObjectiveInputMessage(unsupportedObjectives);
            }
            if (!CheckObjectiveNicknameDuplication(_component.Params.Input[1].Sources.ToArray())) { return false; }
            Objectives = new Objective(_component.Params.Input[1].Sources.ToList());
            return true;
        }

        private static bool ShowNoObjectiveFoundMessage()
        {
            TLog.MethodStart();
            TunnyMessageBox.Show("No objective found.\nPlease connect number or FishPrint to the objective.", "Tunny", MessageBoxButtons.OK, MessageBoxIcon.Error);
            return false;
        }

        private static bool CheckObjectiveNicknameDuplication(IEnumerable<IGH_Param> objectives)
        {
            TLog.MethodStart();
            var nickname = objectives.Select(x => x.NickName)
                                     .GroupBy(name => name).Where(name => name.Count() > 1).Select(group => group.Key).ToList();
            if (nickname.Count > 0)
            {
                TunnyMessageBox.Show("Objective nicknames must be unique.", "Tunny", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return false;
            }
            return true;
        }

        private bool ShowIncorrectObjectiveInputMessage(List<IGH_Param> unsupportedSources)
        {
            TLog.MethodStart();
            TunnyMessageBox.Show("Objective supports only the Number or FishPrint input.\nError input will automatically remove.", "Tunny", MessageBoxButtons.OK, MessageBoxIcon.Error);
            foreach (IGH_Param unsupportedSource in unsupportedSources)
            {
                _component.Params.Input[1].RemoveSource(unsupportedSource);
            }
            _component.ExpireSolution(true);

            return false;
        }

        private bool SetAttributes()
        {
            TLog.MethodStart();
            _attributes = new GH_FishAttribute();
            if (_component.Params.Input[2].SourceCount == 0)
            {
                return true;
            }
            else if (_component.Params.Input[2].SourceCount >= 2 || _component.Params.Input[2].VolatileDataCount > 1)
            {
                return ShowIncorrectAttributeInputMessage();
            }

            IGH_StructureEnumerator enumerator = _component.Params.Input[2].Sources[0].VolatileData.AllData(true);
            foreach (IGH_Goo goo in enumerator)
            {
                if (goo is GH_FishAttribute fishAttr)
                {
                    _attributes = fishAttr;
                    HasConstraint = fishAttr.Value.ContainsKey("Constraint");
                    SetDirection(fishAttr);
                    break;
                }
            }
            return true;
        }

        private void SetDirection(GH_FishAttribute fishAttr)
        {
            int[] directions = new[] { -1 };
            if (fishAttr.Value.TryGetValue("Direction", out object fishDirection))
            {
                if (fishDirection is List<int> dirList)
                {
                    directions = dirList.ToArray();
                }
                else
                {
                    string message = "Direction must be either 1(maximize) or -1(minimize).";
                    TunnyMessageBox.Show(message, "Tunny", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    throw new ArgumentException(message);
                }
            }
            if (!Objectives.SetDirections(directions))
            {
                string message = "The number of the direction in FishAttr must be the same as the number of the objective.";
                TunnyMessageBox.Show(message, "Tunny", MessageBoxButtons.OK, MessageBoxIcon.Error);
                throw new ArgumentException(message);
            }
        }

        private static bool ShowIncorrectAttributeInputMessage()
        {
            TLog.MethodStart();
            TunnyMessageBox.Show("Inputs to Attribute should be grouped together into one FishAttribute.", "Tunny", MessageBoxButtons.OK, MessageBoxIcon.Error);
            return false;
        }

        private void SetCategoryValues(string[] categoryParameters)
        {
            TLog.MethodStart();
            int i = 0;
            foreach (TunnyValueList valueList in _valueLists)
            {
                if (valueList == null)
                {
                    return;
                }
                string[] categories = valueList.ListItems.Select(item => item.Name).ToArray();
                int index = Array.IndexOf(categories, categoryParameters[i++]);
                if (index == -1)
                {
                    return;
                }
                valueList.SelectItemUnsafe(index);
            }
        }

        private void SetSliderValues(decimal[] parameters)
        {
            TLog.MethodStart();
            int i = 0;

            foreach (GH_NumberSlider slider in _sliders)
            {
                decimal val;

                switch (slider.Slider.Type)
                {
                    case GH_SliderAccuracy.Even:
                        val = (int)parameters[i++] * 2;
                        break;
                    case GH_SliderAccuracy.Odd:
                        val = (int)(parameters[i++] * 2) + 1;
                        break;
                    case GH_SliderAccuracy.Integer:
                        val = (int)parameters[i++];
                        break;
                    default:
                        val = parameters[i++];
                        break;
                }

                slider.Slider.RaiseEvents = false;
                slider.SetSliderValue(val);
                slider.ExpireSolution(false);
                slider.Slider.RaiseEvents = true;
            }

            foreach (GalapagosGeneListObject genePool in _genePool)
            {
                for (int j = 0; j < genePool.Count; j++)
                {
                    genePool.set_NormalisedValue(j, GetNormalisedGenePoolValue(parameters[i++], genePool));
                    genePool.ExpireSolution(false);
                }
            }
        }

        private static decimal GetNormalisedGenePoolValue(decimal unnormalized, GalapagosGeneListObject genePool)
        {
            TLog.MethodStart();
            return (unnormalized - genePool.Minimum) / (genePool.Maximum - genePool.Minimum);
        }

        private void Recalculate()
        {
            TLog.MethodStart();
            while (_document.SolutionState != GH_ProcessStep.PreProcess || _document.SolutionDepth != 0) { }
            _document.NewSolution(false);
            while (_document.SolutionState != GH_ProcessStep.PostProcess || _document.SolutionDepth != 0) { }
        }

        public void NewSolution(IList<Parameter> parameters)
        {
            TLog.MethodStart();
            decimal[] decimalParameters = parameters.Where(p => p.HasNumber).Select(p => (decimal)p.Number).ToArray();
            string[] categoryParameters = parameters.Where(p => p.HasCategory).Select(p => p.Category).ToArray();
            SetSliderValues(decimalParameters);
            SetCategoryValues(categoryParameters);
            ExpireInput(_component.Params.Input[1]); // objectives
            ExpireInput(_component.Params.Input[3]); // artifacts
            Recalculate();
            SetObjectives();
            SetAttributes();
            SetArtifacts();
        }

        private void ExpireInput(IGH_Param input)
        {
            TLog.MethodStart();
            // TopLevel is acquired
            // because it is necessary to Expire the component itself, not the value of the output.
            foreach (Guid guid in input.Sources.Select(p => p.InstanceGuid))
            {
                IGH_DocumentObject obj = _document.FindObject(guid, false);
                if (!obj.Attributes.IsTopLevel)
                {
                    Guid topLevelGuid = obj.Attributes.GetTopLevel.InstanceGuid;
                    obj = _document.FindObject(topLevelGuid, true);
                }
                obj.ExpireSolution(false);
            }
        }

        public string[] GetGeometryJson()
        {
            TLog.MethodStart();
            var json = new List<string>();

            if (_attributes.Value == null
                || !_attributes.Value.TryGetValue("Geometry", out object value) || !(value is List<object> geometries))
            {
                return json.ToArray();
            }

            foreach (object param in geometries)
            {
                if (param is IGH_Goo goo)
                {
                    json.Add(GooConverter.GooToString(goo, true));
                }
            }

            return json.ToArray();
        }

        public Dictionary<string, List<string>> GetAttributes()
        {
            TLog.MethodStart();
            var attrs = new Dictionary<string, List<string>>();
            if (_attributes.Value == null)
            {
                return attrs;
            }

            foreach (string key in _attributes.Value.Keys)
            {
                if (key == "Geometry")
                {
                    continue;
                }

                var value = new List<string>();
                if (key == "Constraint")
                {
                    object obj = _attributes.Value[key];
                    if (obj is List<double> val)
                    {
                        value.AddRange(val.Select(v => v.ToString(CultureInfo.InvariantCulture)));
                    }
                }
                else
                {
                    AddGooValues(key, value);
                }
                attrs.Add(key, value);
            }

            return attrs;
        }

        private void AddGooValues(string key, List<string> value)
        {
            TLog.MethodStart();
            if (_attributes.Value[key] is List<object> objList)
            {
                foreach (object param in objList)
                {
                    if (param is IGH_Goo goo)
                    {
                        value.Add(GooConverter.GooToString(goo, true));
                    }
                }
            }
        }

        private bool SetArtifacts()
        {
            TLog.MethodStart();
            Artifacts = new Artifact();
            if (_component.Params.Input[3].SourceCount == 0)
            {
                return true;
            }

            foreach (IGH_Param param in _component.Params.Input[3].Sources)
            {
                IGH_StructureEnumerator enumerator = param.VolatileData.AllData(true);
                foreach (IGH_Goo goo in enumerator)
                {
                    bool result = goo.CastTo(out GeometryBase geometry);
                    if (result)
                    {
                        Artifacts.Geometries.Add(geometry);
                        continue;
                    }

                    if (goo is GH_FishPrint fishPrint)
                    {
                        Artifacts.Images.Add(fishPrint.Value);
                        continue;
                    }

                    result = goo.CastTo(out string path);
                    if (result)
                    {
                        Artifacts.AddFilePathToArtifact(path);
                    }
                }
            }
            return Artifacts.Count() != 0;
        }
    }
}