diff --git a/CLAUDE.md b/CLAUDE.md index d737bd9b..d312aec8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,6 +52,8 @@ Five projects wired together in `CSharpCodeAnalyst.sln`: - **`Tests/`** (project name `CodeParserTests`) — NUnit suite. `ApprovalTests/` parses the `TestSuite/` C# solution once per fixture and asserts on the resulting graph; `UnitTests/` covers cycles, exploration, export, search, architectural rules, etc. - **`ApprovalTestTool/`** — standalone console app that clones external repos listed in `Repositories.txt`, parses each at a pinned commit, hashes the graph dump, and diffs against references. Used to catch parser regressions on real codebases; not part of the CI test run. +`ThirdParty/DsmSuite/` holds a vendored subset (7 of ~38 projects) of the GPL-licensed DsmSuite, which provides the matrix view on the DSM tab. It is foreign code with its own rules — see **Vendored DsmSuite** below before touching anything in there. + `TestSuite/` is a handcrafted C# solution used purely as parser input for the approval tests. Do not consume it from production code — it is intentionally full of odd language constructs. `ReferencedAssemblies/` contains the MSAGL DLLs referenced directly by `CSharpCodeAnalyst.csproj` and `Tests.csproj` (MSAGL is not on NuGet for the versions used here). ## Architectural notes worth knowing before editing @@ -112,6 +114,19 @@ Then wire up the edges: `RuleEngine.Execute` for the evaluation, `RuleViolationV ### Refactoring simulation is destructive `Features/Refactoring/` mutates the in-memory `CodeGraph` directly (move / delete elements, cut edges) and there is no undo. Any code path that offers these operations should save or warn first — see existing command handlers before adding new ones. +### Vendored DsmSuite (the DSM tab) +The matrix on the DSM tab is not ours: `ThirdParty/DsmSuite/` is a modified subset of [DsmSuite](https://github.com/ernstaii/dsmsuite.sourcecode), pinned to a commit recorded in `ThirdParty/DsmSuite/README.md`, GPL-3.0-or-later (originally MIT). Our side is `Features/DsmMatrix/`: `CodeGraphToDsmModelBuilder` fills a DsmSuite `IDsmModel` straight from the `TypeGraph` — explicit `parentId`, no DSI file, no name splitting — and `DsmMatrixView` hosts their `MatrixView`. + +**`ThirdParty/DsmSuite/Dsm.md` is the reference for what the matrix actually draws** — axes (row = provider, column = consumer), the depth-coloured blocks, the two row indicator bars, the weight deciles, and what the builder feeds in. Read it before changing anything about the view's semantics, and keep it in sync: it exists because none of that is discoverable from the UI and it otherwise has to be re-derived from the drawing code. + +**Every change under `ThirdParty/DsmSuite/` costs two things, and neither is optional:** (1) a `Changed for CSharpCodeAnalyst` comment at the site explaining *why*, and (2) a row in the change table in `ThirdParty/DsmSuite/README.md`. GPL §5(a) requires stating what was modified, and that table is the map for ever diffing against upstream again — an undocumented change silently becomes indistinguishable from upstream code. Same rule for the bug list in that README when you find (or fix) a defect in their code. + +Two traps that are already documented there and worth knowing before you write code against their API: +- **`DsmApplication` binds `DsmQueries` to the model it is constructed with and never rebinds.** Populate the model *first*, then construct `DsmApplication`. Calling their `LoadModel` (i.e. the file-based import path) leaves every query running against the previous model. This is why `DsmMatrixView.Show` builds everything from scratch. +- **Their `MainViewModel` is the DataContext**, including its editing commands. Those mutate the DSM model only, never the `CodeGraph`, so they cannot corrupt a parse result — but they are live in the context menus. + +Their resource dictionaries are merged in `App.xaml` at application scope because their controls resolve them via `StaticResource`. Everything in there is keyed; if you pull in more of their XAML, check for implicit (unkeyed) styles first — those would restyle the whole application. + ## Code style - `.editorconfig` is authoritative and ReSharper-tuned: braces required on all `if`/`foreach`/`while`, max line length 199, expression-bodied accessors preferred, `internal` first in modifier order. Analyzer severities default to `none` — do not add warning-as-error enforcement without discussion. diff --git a/CSharpCodeAnalyst.sln b/CSharpCodeAnalyst.sln index 3febaf54..7df2d906 100644 --- a/CSharpCodeAnalyst.sln +++ b/CSharpCodeAnalyst.sln @@ -38,6 +38,22 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpCodeAnalyst.History", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpCodeAnalyst.Contracts", "CSharpCodeAnalyst.Contracts\CSharpCodeAnalyst.Contracts.csproj", "{9DE77B88-294B-45F2-B1A1-6642CC3B4137}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ThirdParty", "ThirdParty", "{356D9C46-07F5-7F13-4EEF-5DD22B885BE8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DsmSuite.Analyzer.Model", "ThirdParty\DsmSuite\DsmSuite.Analyzer.Model\DsmSuite.Analyzer.Model.csproj", "{42652074-77DD-4502-9EF6-D51EBB72A918}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DsmSuite.Common.Model", "ThirdParty\DsmSuite\DsmSuite.Common.Model\DsmSuite.Common.Model.csproj", "{4F8EA064-AF33-4973-A63A-6984CA7C5DF8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DsmSuite.Common.Util", "ThirdParty\DsmSuite\DsmSuite.Common.Util\DsmSuite.Common.Util.csproj", "{FE8B79B8-497F-4939-B07A-6546994DC9EF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DsmSuite.DsmViewer.Application", "ThirdParty\DsmSuite\DsmSuite.DsmViewer.Application\DsmSuite.DsmViewer.Application.csproj", "{3C96864B-93C5-4D75-87FC-98D00A84E32F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DsmSuite.DsmViewer.Model", "ThirdParty\DsmSuite\DsmSuite.DsmViewer.Model\DsmSuite.DsmViewer.Model.csproj", "{D912F0E9-D8C1-43ED-B3DD-F4413AF366AB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DsmSuite.DsmViewer.View", "ThirdParty\DsmSuite\DsmSuite.DsmViewer.View\DsmSuite.DsmViewer.View.csproj", "{9421A097-A4E6-496B-B782-DAB1E819DD0D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DsmSuite.DsmViewer.ViewModel", "ThirdParty\DsmSuite\DsmSuite.DsmViewer.ViewModel\DsmSuite.DsmViewer.ViewModel.csproj", "{A9B11DC9-D36F-48DD-87E8-1C6E547EED5E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -168,10 +184,103 @@ Global {9DE77B88-294B-45F2-B1A1-6642CC3B4137}.Release|x64.Build.0 = Release|Any CPU {9DE77B88-294B-45F2-B1A1-6642CC3B4137}.Release|x86.ActiveCfg = Release|Any CPU {9DE77B88-294B-45F2-B1A1-6642CC3B4137}.Release|x86.Build.0 = Release|Any CPU + {42652074-77DD-4502-9EF6-D51EBB72A918}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42652074-77DD-4502-9EF6-D51EBB72A918}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42652074-77DD-4502-9EF6-D51EBB72A918}.Debug|x64.ActiveCfg = Debug|Any CPU + {42652074-77DD-4502-9EF6-D51EBB72A918}.Debug|x64.Build.0 = Debug|Any CPU + {42652074-77DD-4502-9EF6-D51EBB72A918}.Debug|x86.ActiveCfg = Debug|Any CPU + {42652074-77DD-4502-9EF6-D51EBB72A918}.Debug|x86.Build.0 = Debug|Any CPU + {42652074-77DD-4502-9EF6-D51EBB72A918}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42652074-77DD-4502-9EF6-D51EBB72A918}.Release|Any CPU.Build.0 = Release|Any CPU + {42652074-77DD-4502-9EF6-D51EBB72A918}.Release|x64.ActiveCfg = Release|Any CPU + {42652074-77DD-4502-9EF6-D51EBB72A918}.Release|x64.Build.0 = Release|Any CPU + {42652074-77DD-4502-9EF6-D51EBB72A918}.Release|x86.ActiveCfg = Release|Any CPU + {42652074-77DD-4502-9EF6-D51EBB72A918}.Release|x86.Build.0 = Release|Any CPU + {4F8EA064-AF33-4973-A63A-6984CA7C5DF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F8EA064-AF33-4973-A63A-6984CA7C5DF8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F8EA064-AF33-4973-A63A-6984CA7C5DF8}.Debug|x64.ActiveCfg = Debug|Any CPU + {4F8EA064-AF33-4973-A63A-6984CA7C5DF8}.Debug|x64.Build.0 = Debug|Any CPU + {4F8EA064-AF33-4973-A63A-6984CA7C5DF8}.Debug|x86.ActiveCfg = Debug|Any CPU + {4F8EA064-AF33-4973-A63A-6984CA7C5DF8}.Debug|x86.Build.0 = Debug|Any CPU + {4F8EA064-AF33-4973-A63A-6984CA7C5DF8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F8EA064-AF33-4973-A63A-6984CA7C5DF8}.Release|Any CPU.Build.0 = Release|Any CPU + {4F8EA064-AF33-4973-A63A-6984CA7C5DF8}.Release|x64.ActiveCfg = Release|Any CPU + {4F8EA064-AF33-4973-A63A-6984CA7C5DF8}.Release|x64.Build.0 = Release|Any CPU + {4F8EA064-AF33-4973-A63A-6984CA7C5DF8}.Release|x86.ActiveCfg = Release|Any CPU + {4F8EA064-AF33-4973-A63A-6984CA7C5DF8}.Release|x86.Build.0 = Release|Any CPU + {FE8B79B8-497F-4939-B07A-6546994DC9EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FE8B79B8-497F-4939-B07A-6546994DC9EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE8B79B8-497F-4939-B07A-6546994DC9EF}.Debug|x64.ActiveCfg = Debug|Any CPU + {FE8B79B8-497F-4939-B07A-6546994DC9EF}.Debug|x64.Build.0 = Debug|Any CPU + {FE8B79B8-497F-4939-B07A-6546994DC9EF}.Debug|x86.ActiveCfg = Debug|Any CPU + {FE8B79B8-497F-4939-B07A-6546994DC9EF}.Debug|x86.Build.0 = Debug|Any CPU + {FE8B79B8-497F-4939-B07A-6546994DC9EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FE8B79B8-497F-4939-B07A-6546994DC9EF}.Release|Any CPU.Build.0 = Release|Any CPU + {FE8B79B8-497F-4939-B07A-6546994DC9EF}.Release|x64.ActiveCfg = Release|Any CPU + {FE8B79B8-497F-4939-B07A-6546994DC9EF}.Release|x64.Build.0 = Release|Any CPU + {FE8B79B8-497F-4939-B07A-6546994DC9EF}.Release|x86.ActiveCfg = Release|Any CPU + {FE8B79B8-497F-4939-B07A-6546994DC9EF}.Release|x86.Build.0 = Release|Any CPU + {3C96864B-93C5-4D75-87FC-98D00A84E32F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3C96864B-93C5-4D75-87FC-98D00A84E32F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3C96864B-93C5-4D75-87FC-98D00A84E32F}.Debug|x64.ActiveCfg = Debug|Any CPU + {3C96864B-93C5-4D75-87FC-98D00A84E32F}.Debug|x64.Build.0 = Debug|Any CPU + {3C96864B-93C5-4D75-87FC-98D00A84E32F}.Debug|x86.ActiveCfg = Debug|Any CPU + {3C96864B-93C5-4D75-87FC-98D00A84E32F}.Debug|x86.Build.0 = Debug|Any CPU + {3C96864B-93C5-4D75-87FC-98D00A84E32F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3C96864B-93C5-4D75-87FC-98D00A84E32F}.Release|Any CPU.Build.0 = Release|Any CPU + {3C96864B-93C5-4D75-87FC-98D00A84E32F}.Release|x64.ActiveCfg = Release|Any CPU + {3C96864B-93C5-4D75-87FC-98D00A84E32F}.Release|x64.Build.0 = Release|Any CPU + {3C96864B-93C5-4D75-87FC-98D00A84E32F}.Release|x86.ActiveCfg = Release|Any CPU + {3C96864B-93C5-4D75-87FC-98D00A84E32F}.Release|x86.Build.0 = Release|Any CPU + {D912F0E9-D8C1-43ED-B3DD-F4413AF366AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D912F0E9-D8C1-43ED-B3DD-F4413AF366AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D912F0E9-D8C1-43ED-B3DD-F4413AF366AB}.Debug|x64.ActiveCfg = Debug|Any CPU + {D912F0E9-D8C1-43ED-B3DD-F4413AF366AB}.Debug|x64.Build.0 = Debug|Any CPU + {D912F0E9-D8C1-43ED-B3DD-F4413AF366AB}.Debug|x86.ActiveCfg = Debug|Any CPU + {D912F0E9-D8C1-43ED-B3DD-F4413AF366AB}.Debug|x86.Build.0 = Debug|Any CPU + {D912F0E9-D8C1-43ED-B3DD-F4413AF366AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D912F0E9-D8C1-43ED-B3DD-F4413AF366AB}.Release|Any CPU.Build.0 = Release|Any CPU + {D912F0E9-D8C1-43ED-B3DD-F4413AF366AB}.Release|x64.ActiveCfg = Release|Any CPU + {D912F0E9-D8C1-43ED-B3DD-F4413AF366AB}.Release|x64.Build.0 = Release|Any CPU + {D912F0E9-D8C1-43ED-B3DD-F4413AF366AB}.Release|x86.ActiveCfg = Release|Any CPU + {D912F0E9-D8C1-43ED-B3DD-F4413AF366AB}.Release|x86.Build.0 = Release|Any CPU + {9421A097-A4E6-496B-B782-DAB1E819DD0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9421A097-A4E6-496B-B782-DAB1E819DD0D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9421A097-A4E6-496B-B782-DAB1E819DD0D}.Debug|x64.ActiveCfg = Debug|Any CPU + {9421A097-A4E6-496B-B782-DAB1E819DD0D}.Debug|x64.Build.0 = Debug|Any CPU + {9421A097-A4E6-496B-B782-DAB1E819DD0D}.Debug|x86.ActiveCfg = Debug|Any CPU + {9421A097-A4E6-496B-B782-DAB1E819DD0D}.Debug|x86.Build.0 = Debug|Any CPU + {9421A097-A4E6-496B-B782-DAB1E819DD0D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9421A097-A4E6-496B-B782-DAB1E819DD0D}.Release|Any CPU.Build.0 = Release|Any CPU + {9421A097-A4E6-496B-B782-DAB1E819DD0D}.Release|x64.ActiveCfg = Release|Any CPU + {9421A097-A4E6-496B-B782-DAB1E819DD0D}.Release|x64.Build.0 = Release|Any CPU + {9421A097-A4E6-496B-B782-DAB1E819DD0D}.Release|x86.ActiveCfg = Release|Any CPU + {9421A097-A4E6-496B-B782-DAB1E819DD0D}.Release|x86.Build.0 = Release|Any CPU + {A9B11DC9-D36F-48DD-87E8-1C6E547EED5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A9B11DC9-D36F-48DD-87E8-1C6E547EED5E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A9B11DC9-D36F-48DD-87E8-1C6E547EED5E}.Debug|x64.ActiveCfg = Debug|Any CPU + {A9B11DC9-D36F-48DD-87E8-1C6E547EED5E}.Debug|x64.Build.0 = Debug|Any CPU + {A9B11DC9-D36F-48DD-87E8-1C6E547EED5E}.Debug|x86.ActiveCfg = Debug|Any CPU + {A9B11DC9-D36F-48DD-87E8-1C6E547EED5E}.Debug|x86.Build.0 = Debug|Any CPU + {A9B11DC9-D36F-48DD-87E8-1C6E547EED5E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A9B11DC9-D36F-48DD-87E8-1C6E547EED5E}.Release|Any CPU.Build.0 = Release|Any CPU + {A9B11DC9-D36F-48DD-87E8-1C6E547EED5E}.Release|x64.ActiveCfg = Release|Any CPU + {A9B11DC9-D36F-48DD-87E8-1C6E547EED5E}.Release|x64.Build.0 = Release|Any CPU + {A9B11DC9-D36F-48DD-87E8-1C6E547EED5E}.Release|x86.ActiveCfg = Release|Any CPU + {A9B11DC9-D36F-48DD-87E8-1C6E547EED5E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {42652074-77DD-4502-9EF6-D51EBB72A918} = {356D9C46-07F5-7F13-4EEF-5DD22B885BE8} + {4F8EA064-AF33-4973-A63A-6984CA7C5DF8} = {356D9C46-07F5-7F13-4EEF-5DD22B885BE8} + {FE8B79B8-497F-4939-B07A-6546994DC9EF} = {356D9C46-07F5-7F13-4EEF-5DD22B885BE8} + {3C96864B-93C5-4D75-87FC-98D00A84E32F} = {356D9C46-07F5-7F13-4EEF-5DD22B885BE8} + {D912F0E9-D8C1-43ED-B3DD-F4413AF366AB} = {356D9C46-07F5-7F13-4EEF-5DD22B885BE8} + {9421A097-A4E6-496B-B782-DAB1E819DD0D} = {356D9C46-07F5-7F13-4EEF-5DD22B885BE8} + {A9B11DC9-D36F-48DD-87E8-1C6E547EED5E} = {356D9C46-07F5-7F13-4EEF-5DD22B885BE8} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2BBCF792-C019-4195-AF08-45CC642B8A18} EndGlobalSection diff --git a/CSharpCodeAnalyst/App.xaml b/CSharpCodeAnalyst/App.xaml index 791663f6..d02a595a 100644 --- a/CSharpCodeAnalyst/App.xaml +++ b/CSharpCodeAnalyst/App.xaml @@ -1,7 +1,28 @@  + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:themes="clr-namespace:DsmSuite.DsmViewer.View.Resources.Themes;assembly=DsmSuite.DsmViewer.View"> - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CSharpCodeAnalyst/CSharpCodeAnalyst.csproj b/CSharpCodeAnalyst/CSharpCodeAnalyst.csproj index 38909381..30b0d00c 100644 --- a/CSharpCodeAnalyst/CSharpCodeAnalyst.csproj +++ b/CSharpCodeAnalyst/CSharpCodeAnalyst.csproj @@ -158,6 +158,10 @@ + + + + diff --git a/CSharpCodeAnalyst/Features/DsmMatrix/CodeGraphToDsmModelBuilder.cs b/CSharpCodeAnalyst/Features/DsmMatrix/CodeGraphToDsmModelBuilder.cs new file mode 100644 index 00000000..6374ffa3 --- /dev/null +++ b/CSharpCodeAnalyst/Features/DsmMatrix/CodeGraphToDsmModelBuilder.cs @@ -0,0 +1,246 @@ +using CSharpCodeAnalyst.CodeGraph.Algorithms.Metrics; +using CSharpCodeAnalyst.CodeGraph.Graph; +using DsmSuite.DsmViewer.Application.Sorting; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace CSharpCodeAnalyst.Features.DsmMatrix; + +/// +/// Populates a DsmSuite from a , so the +/// embedded matrix view can show the current solution without any file round trip. +/// +/// +/// The topology comes from : one vertex per internal type, dependencies lifted to +/// the containing type and deduplicated, external types and self edges dropped. The hierarchy above the +/// types (namespaces, assemblies) is taken from the code graph's parent chain rather than reconstructed +/// from dotted names, which is what DsmSuite's own DSI import has to do for lack of parent information. +/// +public sealed class CodeGraphToDsmModelBuilder +{ + private const string RelationType = "Dependency"; + + private readonly CodeGraph.Graph.CodeGraph _codeGraph; + private readonly IDsmModel _dsmModel; + + /// Maps a code element id to the DSM element created for it. + private readonly Dictionary _dsmElementsByCodeElementId = []; + + /// Namespaces left out of the model, see . + private HashSet _passThroughNamespaces = []; + + public CodeGraphToDsmModelBuilder(IDsmModel dsmModel, CodeGraph.Graph.CodeGraph codeGraph) + { + _dsmModel = dsmModel; + _codeGraph = codeGraph; + } + + /// + /// Fills the model. Returns the number of types added, so the caller can tell an empty result from a + /// populated one without querying the model. + /// + public int Build() + { + _dsmModel.Clear(); + _dsmElementsByCodeElementId.Clear(); + + var typeGraph = TypeGraph.Build(_codeGraph); + _passThroughNamespaces = FindPassThroughNamespaces(typeGraph); + + foreach (var typeId in typeGraph.Vertices) + { + AddWithAncestors(typeId); + } + + AddRelations(typeGraph); + + Partition(_dsmModel.RootElement); + _dsmModel.AssignElementOrder(); + return typeGraph.VertexCount; + } + + /// + /// The namespaces that carry no structure of their own: exactly one child, and that child another + /// namespace. They do not become elements; their name is merged into the first descendant that does + /// (see ), so the level disappears while the label stays a real namespace. + /// + /// + /// The parser creates one element per namespace segment, so a project whose root namespace repeats its + /// assembly name produces a chain of pass-throughs: assembly "A.B" holds namespace "A" holds namespace + /// "B" holds the real ones. Collapsing them is a view concern, which is why this lives here rather than + /// in the parser, whose hierarchy is right and is what the tree view wants. + /// + /// What such a level costs, given that it holds nothing: + /// + /// + /// + /// An expand that reveals nothing. Expanding the element yields exactly one row, carrying the + /// same numbers as before — weights aggregate over the children and there is only one child — + /// so it takes another click to see anything. + /// + /// + /// A vertical strip down the left of the matrix, taking width and saying nothing. + /// + /// + /// One of only four depth colours. MatrixColorConverter.GetColor computes + /// depth % 4, so an empty level shifts the whole ramp and brings it round sooner, + /// costing the depth-coloured blocks the contrast they read by. That is the matrix' main + /// reading aid: inside the block is internal, outside crosses the boundary. + /// + /// + /// + /// Note it is not about duplicate rows: rows are the collapsed elements, expanded ones become the + /// vertical strips, so a level and its child are never on screen as rows at the same time. + /// + /// + /// "Exactly one child" has to be counted over what actually reaches the model, not over the code + /// graph: a namespace holding two types of which one is external is a pass-through here even + /// though the code graph shows it branching. + /// + /// + private HashSet FindPassThroughNamespaces(TypeGraph typeGraph) + { + // Everything the model will hold: the types, plus every ancestor they hang from. + var included = new HashSet(); + foreach (var typeId in typeGraph.Vertices) + { + var current = _codeGraph.Nodes[typeId]; + while (current is not null && included.Add(current.Id)) + { + current = current.Parent; + } + } + + var childrenByParent = new Dictionary>(); + foreach (var element in included.Select(id => _codeGraph.Nodes[id])) + { + if (element.Parent is null) + { + continue; + } + + if (!childrenByParent.TryGetValue(element.Parent.Id, out var siblings)) + { + siblings = []; + childrenByParent[element.Parent.Id] = siblings; + } + + siblings.Add(element); + } + + var passThrough = new HashSet(); + foreach (var element in included.Select(id => _codeGraph.Nodes[id])) + { + if (element.ElementType is not CodeElementType.Namespace) + { + continue; + } + + if (childrenByParent.TryGetValue(element.Id, out var children) && + children is [{ ElementType: CodeElementType.Namespace }]) + { + passThrough.Add(element.Id); + } + } + + return passThrough; + } + + /// + /// Orders the children of every element so that dependencies line up on one side of the diagonal. + /// + /// + /// Without this the rows sit in whatever order the elements were added, which for us is the iteration + /// order of a hash set — so an acyclic structure looks just as scattered as a tangled one. The + /// partitioning is what turns "no cycles" into the triangular shape that makes it visible. Sorting is + /// per parent: children are only ever reordered among their siblings, so the hierarchy is untouched. + /// Mirrors ImporterBase.Partition, which we cannot reuse (protected, and tied to the DSI import). + /// + private void Partition(IDsmElement element) + { + var algorithm = SortAlgorithmFactory.CreateAlgorithm(_dsmModel, element, PartitionSortAlgorithm.AlgorithmName); + _dsmModel.ReorderChildren(element, algorithm.Sort()); + + foreach (var child in element.Children) + { + Partition(child); + } + } + + /// + /// Returns the DSM element for a code element, creating it and every missing ancestor first. Walking up + /// before creating keeps a parent's id available by the time the child needs it. For a pass-through + /// namespace it returns the nearest kept ancestor instead, which is what drops it from the model. + /// + private IDsmElement? AddWithAncestors(string codeElementId) + { + if (_passThroughNamespaces.Contains(codeElementId)) + { + var skipped = _codeGraph.Nodes[codeElementId].Parent; + return skipped is null ? null : AddWithAncestors(skipped.Id); + } + + if (_dsmElementsByCodeElementId.TryGetValue(codeElementId, out var existing)) + { + return existing; + } + + var codeElement = _codeGraph.Nodes[codeElementId]; + var parent = codeElement.Parent is null ? null : AddWithAncestors(codeElement.Parent.Id); + + var dsmElement = _dsmModel.AddElement( + MergedName(codeElement), + codeElement.ElementType.ToString(), + parent?.Id, + null, + null); + + _dsmElementsByCodeElementId[codeElementId] = dsmElement; + return dsmElement; + } + + /// + /// The element's name, prefixed with the pass-through ancestors that were dropped, so that + /// A -> B -> C with A and B pass-through becomes one element named A.B.C. + /// + /// + /// Dropping a pass-through level and dropping its name are separate decisions, and only the first one + /// buys anything: the tree has the same shape whether the name is merged in or thrown away. Merging + /// keeps the invariant that a label is the namespace path relative to the element it sits in — one + /// segment where nothing was collapsed, the collapsed chain where something was. Throwing the name + /// away breaks it: the row under assembly "CSharpCodeAnalyst.CodeGraph" would read "CodeGraph", which + /// names nothing, where it now reads "CSharpCodeAnalyst.CodeGraph", which is the namespace. Assembly + /// names keep their dots for the same reason. + /// + /// Types are never affected: a namespace holding a type has a non-namespace child and is + /// therefore not a pass-through, so the parent of a type is never dropped. + /// + /// + private string MergedName(CodeElement element) + { + var parts = new List { element.Name }; + + var current = element.Parent; + while (current is not null && _passThroughNamespaces.Contains(current.Id)) + { + parts.Insert(0, current.Name); + current = current.Parent; + } + + return string.Join(".", parts); + } + + private void AddRelations(TypeGraph typeGraph) + { + foreach (var (consumerId, providerIds) in typeGraph.Out) + { + var consumer = _dsmElementsByCodeElementId[consumerId]; + foreach (var providerId in providerIds) + { + // TypeGraph deduplicates, so every edge carries the same weight. Once it can report how + // many relationships a type level edge stands for, that count belongs here: it is what + // drives the matrix' cell weights. + _dsmModel.AddRelation(consumer, _dsmElementsByCodeElementId[providerId], RelationType, 1, null); + } + } + } +} diff --git a/CSharpCodeAnalyst/Features/DsmMatrix/DsmMatrixFactory.cs b/CSharpCodeAnalyst/Features/DsmMatrix/DsmMatrixFactory.cs new file mode 100644 index 00000000..d501e470 --- /dev/null +++ b/CSharpCodeAnalyst/Features/DsmMatrix/DsmMatrixFactory.cs @@ -0,0 +1,41 @@ +using System.Reflection; +using DsmSuite.DsmViewer.Application.Core; +using DsmSuite.DsmViewer.Model.Core; + +// Both this application and DsmSuite have a MainViewModel. +using DsmMainViewModel = DsmSuite.DsmViewer.ViewModel.Main.MainViewModel; + +namespace CSharpCodeAnalyst.Features.DsmMatrix; + +/// +/// Builds the view model behind the DSM tab from a code graph. +/// +public static class DsmMatrixFactory +{ + /// + /// Builds everything from scratch and hands back a view model ready to be shown. + /// + /// + /// Safe to call off the UI thread, and meant to be: the work is quadratic in the number of types + /// (MatrixViewModel's constructor fills a weight and a colour for every cell), which is why the + /// caller runs it in the background with a progress indicator. Nothing in here touches a + /// DispatcherObject — DsmSuite's view models raise PropertyChanged into the void until something + /// binds to them, and their commands are CommunityToolkit RelayCommands, which have no dispatcher + /// affinity. Hand the result to the UI thread and bind it there. + /// + /// The order matters: DsmApplication binds its query layer to the model instance it is + /// constructed with and never rebinds, so the model has to be fully populated first. + /// + /// + public static DsmMainViewModel Create(CodeGraph.Graph.CodeGraph codeGraph) + { + var model = new DsmModel("CSharpCodeAnalyst", Assembly.GetExecutingAssembly()); + var typeCount = new CodeGraphToDsmModelBuilder(model, codeGraph).Build(); + + var application = new DsmApplication(model); + var viewModel = new DsmMainViewModel(application); + viewModel.ShowInMemoryModel($"{typeCount} types"); + + return viewModel; + } +} diff --git a/CSharpCodeAnalyst/Features/DsmMatrix/DsmMatrixTheme.xaml b/CSharpCodeAnalyst/Features/DsmMatrix/DsmMatrixTheme.xaml new file mode 100644 index 00000000..7359a0db --- /dev/null +++ b/CSharpCodeAnalyst/Features/DsmMatrix/DsmMatrixTheme.xaml @@ -0,0 +1,64 @@ + + + + + #1F2933 + + + #E4E7EA + + + #D3DCE3 + #B4C3CE + #93A8B7 + #748C9E + + + #CF5C3C + + + #6FA84B + #4A8DC4 + + + #C2185B + #7B3FA0 + + + #37474F + + + 240 + diff --git a/CSharpCodeAnalyst/Features/DsmMatrix/DsmMatrixView.xaml b/CSharpCodeAnalyst/Features/DsmMatrix/DsmMatrixView.xaml new file mode 100644 index 00000000..fa015fe2 --- /dev/null +++ b/CSharpCodeAnalyst/Features/DsmMatrix/DsmMatrixView.xaml @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/CSharpCodeAnalyst/Features/DsmMatrix/DsmMatrixView.xaml.cs b/CSharpCodeAnalyst/Features/DsmMatrix/DsmMatrixView.xaml.cs new file mode 100644 index 00000000..687c477e --- /dev/null +++ b/CSharpCodeAnalyst/Features/DsmMatrix/DsmMatrixView.xaml.cs @@ -0,0 +1,61 @@ +using System.Windows.Controls; +using System.Windows.Input; + +// Both this application and DsmSuite have a MainViewModel. +using DsmMainViewModel = DsmSuite.DsmViewer.ViewModel.Main.MainViewModel; + +namespace CSharpCodeAnalyst.Features.DsmMatrix; + +/// +/// Hosts DsmSuite's matrix view. Expects DsmSuite's own MainViewModel as its DataContext; see +/// for how the code graph becomes one. +/// +public partial class DsmMatrixView : UserControl +{ + /// + /// Zoom range for ctrl+wheel. The lower bound is what lets a large matrix be shrunk down to the + /// window: illegible, but the shape (layering, cycle blocks near the diagonal) still reads. It is far + /// below DsmSuite's own 0.5, which only bounds their toolbar zoom commands — we set ZoomLevel + /// directly and clamp here instead. + /// + private const double MinZoom = 0.04; + + private const double MaxZoom = 4.0; + private const double ZoomStep = 1.15; + + public DsmMatrixView() + { + InitializeComponent(); + PreviewMouseWheel += OnPreviewMouseWheel; + } + + /// + /// Ctrl+wheel zooms the whole matrix. The scale itself is DsmSuite's: MatrixView already carries a + /// ScaleTransform bound to MatrixViewModel.ZoomLevel, as a LayoutTransform — so zooming out re-runs + /// layout at the larger logical size and genuinely fits more matrix into the window, rather than just + /// shrinking the viewport. Only the operation was missing, because it sits on the viewer's own + /// toolbar, which we do not host. + /// + /// + /// Deliberately ctrl+wheel, not plain wheel: the cells live in a ScrollViewer, and plain wheel has to + /// stay with scrolling, which is what you need at a readable zoom level. + /// + private void OnPreviewMouseWheel(object sender, MouseWheelEventArgs e) + { + if (Keyboard.Modifiers != ModifierKeys.Control) + { + return; + } + + if (DataContext is not DsmMainViewModel { ActiveMatrix: not null } viewModel) + { + return; + } + + var factor = e.Delta > 0 ? ZoomStep : 1.0 / ZoomStep; + viewModel.ActiveMatrix.ZoomLevel = Math.Clamp(viewModel.ActiveMatrix.ZoomLevel * factor, MinZoom, MaxZoom); + + // Otherwise the ScrollViewer underneath scrolls as well. + e.Handled = true; + } +} diff --git a/CSharpCodeAnalyst/MainViewModel.cs b/CSharpCodeAnalyst/MainViewModel.cs index 04670be7..5f825304 100644 --- a/CSharpCodeAnalyst/MainViewModel.cs +++ b/CSharpCodeAnalyst/MainViewModel.cs @@ -23,6 +23,7 @@ using CSharpCodeAnalyst.Features.Ai; using CSharpCodeAnalyst.Features.Analyzers; using CSharpCodeAnalyst.Features.CycleGroups; +using CSharpCodeAnalyst.Features.DsmMatrix; using CSharpCodeAnalyst.Features.Export; using CSharpCodeAnalyst.Features.Gallery; using CSharpCodeAnalyst.Features.Graph; @@ -47,11 +48,17 @@ using CSharpCodeAnalyst.Shared.Wpf; using CSharpCodeAnalyst.TreeMap; +// Both this application and DsmSuite have a MainViewModel. +using DsmMainViewModel = DsmSuite.DsmViewer.ViewModel.Main.MainViewModel; + namespace CSharpCodeAnalyst; internal sealed class MainViewModel : INotifyPropertyChanged { + /// There is only ever one matrix tab, it always shows the whole graph. + private const string DsmTabId = "Dsm"; + private readonly AiAdvisorService _aiAdvisorService = new(); private readonly AnalyzerManager _analyzerManager; @@ -146,6 +153,7 @@ internal MainViewModel(MessageBus messaging, AppSettings settings, UserPreferenc GraphRefitCommand = new WpfCommand(OnGraphRefit); StopRenderingCommand = new WpfCommand(OnStopRendering); FindCyclesCommand = new WpfCommand(OnFindCycles); + ShowDsmCommand = new WpfCommand(OnShowDsm); AiAdviseCommand = new WpfCommand(OnAiAdvise); ExecuteAnalyzerCommand = new WpfCommand(OnExecuteAnalyzer); @@ -270,6 +278,7 @@ private set public ICommand ExportToPlantUmlCommand { get; } public ICommand ExportToSvgCommand { get; set; } public ICommand FindCyclesCommand { get; } + public ICommand ShowDsmCommand { get; } public ICommand AiAdviseCommand { get; } public ICommand ExportToDsiCommand { get; } public ICommand SearchCommand { get; } @@ -710,6 +719,66 @@ private void ShowHierarchicalTab(string id, string title, HierarchicalDataContex DynamicTabActivated?.Invoke(tab); } + /// + /// Same as , but for the dependency structure matrix. There is only ever + /// one of it — it always shows the whole graph — so the id is fixed. + /// + private void ShowDsmTab(DsmMainViewModel matrix) + { + var tab = DynamicTabs.OfType().FirstOrDefault(t => t.Id == DsmTabId); + if (tab is null) + { + tab = new DsmTabViewModel(DsmTabId, Strings.Dsm_TabHeader, matrix); + tab.CloseCommand = new WpfCommand(() => DynamicTabs.Remove(tab)); + DynamicTabs.Add(tab); + } + else + { + tab.Matrix = matrix; + } + + DynamicTabActivated?.Invoke(tab); + } + + /// + /// Builds the dependency structure matrix over the whole graph and shows it in its own tab. + /// + /// + /// Off the UI thread with a progress indicator, because the cost is quadratic in the number of types: + /// the matrix holds a weight and a colour per cell. See for why + /// building the view model in the background is safe. + /// + private async void OnShowDsm() + { + if (_codeGraph is null) + { + return; + } + + DsmMainViewModel? matrix = null; + var codeGraph = _codeGraph; + try + { + IsLoading = true; + LoadMessage = Strings.BuildingDsm_Message; + await Task.Run(() => { matrix = DsmMatrixFactory.Create(codeGraph); }); + } + catch (Exception ex) + { + _ui.ShowError(string.Format(Strings.OperationFailed_Message, ex.Message)); + } + finally + { + LoadMessage = string.Empty; + IsLoading = false; + } + + if (matrix is not null) + { + ShowDsmTab(matrix); + } + } + private async void OnFindCycles() { if (_codeGraph is null) diff --git a/CSharpCodeAnalyst/MainWindow.xaml b/CSharpCodeAnalyst/MainWindow.xaml index 5934d01c..2f309af0 100644 --- a/CSharpCodeAnalyst/MainWindow.xaml +++ b/CSharpCodeAnalyst/MainWindow.xaml @@ -8,6 +8,7 @@ mc:Ignorable="d" xmlns:resources="clr-namespace:CSharpCodeAnalyst.Resources" xmlns:dynamicDataGrid1="clr-namespace:CSharpCodeAnalyst.Shared.DynamicDataGrid" + xmlns:dsmMatrix="clr-namespace:CSharpCodeAnalyst.Features.DsmMatrix" xmlns:tree="clr-namespace:CSharpCodeAnalyst.Features.Tree" xmlns:advancedSearch="clr-namespace:CSharpCodeAnalyst.Features.AdvancedSearch" xmlns:info="clr-namespace:CSharpCodeAnalyst.Features.Info" @@ -53,6 +54,12 @@ UserCommands="{Binding Commands}"/> + + + + + @@ -130,6 +137,16 @@ LargeImageSource="/Resources/save_project.png" Command="{Binding SaveProjectCommand}" KeyTip="SA" /> + + + + + + + { @@ -56,7 +57,12 @@ private void InitializeDynamicTabs(MainViewModel mainVm) { foreach (ITabViewModel tab in e.NewItems!) { - var contentTemplate = tab is HierarchicalTabViewModel ? hierarchicalContentTemplate : tabularContentTemplate; + var contentTemplate = tab switch + { + HierarchicalTabViewModel => hierarchicalContentTemplate, + DsmTabViewModel => dsmContentTemplate, + _ => tabularContentTemplate + }; WorkingArea.Items.Add(new TabItem { Header = tab, diff --git a/CSharpCodeAnalyst/Resources/Strings.Designer.cs b/CSharpCodeAnalyst/Resources/Strings.Designer.cs index bcf74818..ba111665 100644 --- a/CSharpCodeAnalyst/Resources/Strings.Designer.cs +++ b/CSharpCodeAnalyst/Resources/Strings.Designer.cs @@ -3400,5 +3400,41 @@ public static string WebView_TabHeader { return ResourceManager.GetString("WebView_TabHeader", resourceCulture); } } + + /// + /// Looks up a localized string similar to DSM. + /// + public static string Dsm_Label { + get { + return ResourceManager.GetString("Dsm_Label", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Shows the whole solution as a dependency structure matrix, on the type graph: one row and column per type, dependencies lifted to the containing type, external types left out. + /// + public static string Dsm_Tooltip { + get { + return ResourceManager.GetString("Dsm_Tooltip", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DSM. + /// + public static string Dsm_TabHeader { + get { + return ResourceManager.GetString("Dsm_TabHeader", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Building dependency structure matrix .... + /// + public static string BuildingDsm_Message { + get { + return ResourceManager.GetString("BuildingDsm_Message", resourceCulture); + } + } } } diff --git a/CSharpCodeAnalyst/Resources/Strings.resx b/CSharpCodeAnalyst/Resources/Strings.resx index 8974ce0e..c886d3b3 100644 --- a/CSharpCodeAnalyst/Resources/Strings.resx +++ b/CSharpCodeAnalyst/Resources/Strings.resx @@ -1257,5 +1257,17 @@ Do you want to overwrite it? Image Viewer + + DSM + + + Shows the whole solution as a dependency structure matrix, on the type graph: one row and column per type, dependencies lifted to the containing type, external types left out. + + + DSM + + + Building dependency structure matrix ... + \ No newline at end of file diff --git a/CSharpCodeAnalyst/Shared/Tabs/DsmTabViewModel.cs b/CSharpCodeAnalyst/Shared/Tabs/DsmTabViewModel.cs new file mode 100644 index 00000000..1809bec4 --- /dev/null +++ b/CSharpCodeAnalyst/Shared/Tabs/DsmTabViewModel.cs @@ -0,0 +1,63 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Windows.Input; + +// Both this application and DsmSuite have a MainViewModel. +using DsmMainViewModel = DsmSuite.DsmViewer.ViewModel.Main.MainViewModel; + +namespace CSharpCodeAnalyst.Shared.Tabs; + +/// +/// A tab created on demand for the dependency structure matrix, keyed by so that +/// rebuilding the matrix updates the existing tab in place instead of creating a duplicate. Owned by an +/// on MainViewModel; +/// MainWindow's code-behind projects that collection onto the working-area TabControl. +/// +/// +/// Unlike the other tabs this one carries a whole view model rather than data: the matrix is DsmSuite's +/// own MainViewModel, and their MatrixView expects to find it as its DataContext. +/// +public sealed class DsmTabViewModel(string id, string title, DsmMainViewModel matrix) : ITabViewModel +{ + public DsmMainViewModel Matrix + { + get; + set + { + if (ReferenceEquals(field, value)) + { + return; + } + + field = value; + OnPropertyChanged(); + } + } = matrix; + + public string Id { get; } = id; + + public string Title + { + get; + set + { + if (field == value) + { + return; + } + + field = value; + OnPropertyChanged(); + } + } = title; + + /// Set by the owner (MainViewModel) right after construction. + public ICommand? CloseCommand { get; set; } + + public event PropertyChangedEventHandler? PropertyChanged; + + private void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} diff --git a/Documentation/Images/dsm-suite.png b/Documentation/Images/dsm-suite.png index 29e8d010..f4cef3f4 100644 Binary files a/Documentation/Images/dsm-suite.png and b/Documentation/Images/dsm-suite.png differ diff --git a/README.md b/README.md index 91502142..c2701c82 100644 --- a/README.md +++ b/README.md @@ -167,12 +167,12 @@ The PlantUML syntax is copied to the clipboard. You can use any online editor to ![](Documentation/Images/example-uml.png) ---- - ### Plain Text Plain text might sound boring, but it's actually useful. If you feed a large language model your whole source code for refactoring, you’ll quickly run out of tokens or get garbage results. Instead, just give the LLM your prompt along with this text-based dependency graph. This greatly improves results and saves a massive amount of tokens. The LLM doesn't even need a description of the graph. +--- + ## Validate architectural rules Codebases tend to decay over time if no one monitors the boundaries. With Architectural Rules, you can define strict guardrails (like "UI must not touch the Data layer") and automatically check your solution for violations. @@ -298,6 +298,25 @@ To integrate the tool into a build pipeline, you can call it without a user inte --- +## Dependency Structure Matrix (DSM) + +A Dependency Structure Matrix (DSM) displays system dependencies in a compact grid. Based on the convention used here, a numbered cell indicates that the element at the top (column) depends on the element on the left (row). + +To learn how to read the matrix and spot architectural patterns, you can refer to the [DSM Suite Overview](https://dsmsuite.github.io/dsm_overview). Getting used to this view may require a bit of practice. + +> **Note:** The matrix does not show the entire raw codebase, but rather the type graph. For example, dependencies between individual methods are lifted to their corresponding types (this is the exact same graph used to calculate our system metrics). This abstraction keeps the DSM focused and clean. + +![](Documentation/Images/dsm-suite.png) + +- **Ctrl + mouse wheel** zooms the whole matrix (0.04 – 4.0). Plain wheel scrolls. +- **Plain wheel** scrolls up and down, **shift + wheel** sideways — from anywhere in the matrix, the headers + included. Worth knowing because the scroll bars sit *inside* the scaled grid, so zooming out shrinks them + along with everything else until they are too thin to grab. +- **Click the arrow** in a row header to expand or collapse; hold **shift** and it + works recursively. + +--- + ## Metrics C# Code Analyst can calculate a few key metrics that matter. @@ -308,6 +327,7 @@ All metrics are accessible via the Analyzer Ribbon, and the results are presente ![](Documentation/Images/metrics-example.png) + ## Other languages The tool is built for C#, but you can also import Jdeps output to get a basic visualization of Java code. @@ -316,6 +336,8 @@ The tool is built for C#, but you can also import Jdeps output to get a basic vi jdeps.exe -verbose:class ... >jdeps.txt ``` +--- + ## Analyze a GIT repository Static analysis is great, but it misses how code evolves over time. Inspired by Adam Tornhill’s *Your Code as a Crime Scene*, this tool includes Git history analysis to uncover architectural patterns that no static analyzer can see – like change coupling and hotspots. To read more about these ideas, see also my other repository: https://github.com/ATrefzer/Insight. @@ -370,6 +392,10 @@ https://github.com/punker76/gong-wpf-dragdrop - Markdown rendering in the AI Advisor window is powered by **Markdig.Wpf** and **Markdig**. Copyright (c) Nicolas Musset and Alexandre Mutel. Licensed under BSD-2-Clause. https://github.com/Kryptos-FR/markdig.wpf / https://github.com/xoofx/markdig +- The dependency structure matrix on the DSM tab is the viewer from **DsmSuite**, licensed under +GPL-3.0-or-later (same as this project) and originally MIT-licensed by jmuijsenberg. A modified +subset of it is vendored under [ThirdParty/DsmSuite](ThirdParty/DsmSuite/). +https://github.com/ernstaii/dsmsuite.sourcecode / https://github.com/dsmsuite/dsmsuite.sourcecode For complete third-party license information, see the [ThirdPartyNotices](ThirdPartyNotices/) folder. diff --git a/Tests/UnitTests/DsmMatrix/CodeGraphToDsmModelBuilderTests.cs b/Tests/UnitTests/DsmMatrix/CodeGraphToDsmModelBuilderTests.cs new file mode 100644 index 00000000..31ecd348 --- /dev/null +++ b/Tests/UnitTests/DsmMatrix/CodeGraphToDsmModelBuilderTests.cs @@ -0,0 +1,285 @@ +using System.Reflection; +using CodeParserTests.Helper; +using CSharpCodeAnalyst.CodeGraph.Graph; +using CSharpCodeAnalyst.Features.DsmMatrix; +using DsmSuite.DsmViewer.Model.Core; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace CodeParserTests.UnitTests.DsmMatrix; + +[TestFixture] +public class CodeGraphToDsmModelBuilderTests +{ + [SetUp] + public void SetUp() + { + _graph = new TestCodeGraph(); + _dsmModel = new DsmModel("Test", Assembly.GetExecutingAssembly()); + } + + private TestCodeGraph _graph = null!; + private IDsmModel _dsmModel = null!; + + private static void Rel(CodeElement source, CodeElement target, RelationshipType type) + { + source.Relationships.Add(new Relationship(source.Id, target.Id, type)); + } + + private int Build() + { + return new CodeGraphToDsmModelBuilder(_dsmModel, _graph).Build(); + } + + /// Elements in the model, without the implicit root that DsmModel always carries. + private int AddedElementCount() + { + return _dsmModel.GetElementCount() - 1; + } + + [Test] + public void Build_EmptyGraph_AddsNothing() + { + Assert.That(Build(), Is.EqualTo(0)); + Assert.That(AddedElementCount(), Is.EqualTo(0)); + } + + [Test] + public void Build_TypeUnderNamespace_RecreatesHierarchyFromParentChain() + { + var assembly = _graph.CreateAssembly("Asm"); + var ns = _graph.CreateNamespace("Ns", assembly); + _graph.CreateClass("A", ns); + + Build(); + + // The full name must come from the parent chain, not from splitting a dotted name. + var element = _dsmModel.GetElementByFullname("Asm.Ns.A"); + Assert.That(element, Is.Not.Null); + Assert.That(element.Type, Is.EqualTo(CodeElementType.Class.ToString())); + } + + [Test] + public void Build_TypeNameContainsDots_StaysASingleElement() + { + // This is what the DSI detour cannot do: a name with dots would become a hierarchy there. + var assembly = _graph.CreateAssembly("Asm"); + _graph.CreateClass("Weird", assembly, "Asm.Weird"); + _graph.Nodes["Weird"].Parent!.Children.Add(_graph.Nodes["Weird"]); + + Build(); + + Assert.That(AddedElementCount(), Is.EqualTo(2), "assembly plus one type, no split"); + } + + [Test] + public void Build_MethodCallsBetweenTypes_LiftedToOneTypeRelation() + { + var a = _graph.CreateClass("A"); + var m1 = _graph.CreateMethod("A.M1", a); + var m2 = _graph.CreateMethod("A.M2", a); + var b = _graph.CreateClass("B"); + var bm = _graph.CreateMethod("B.M", b); + + Rel(m1, bm, RelationshipType.Calls); + Rel(m2, bm, RelationshipType.Calls); + + Assert.That(Build(), Is.EqualTo(2)); + + var consumer = _dsmModel.GetElementByFullname("A"); + var provider = _dsmModel.GetElementByFullname("B"); + Assert.Multiple(() => + { + Assert.That(_dsmModel.GetRelationCount(consumer, provider), Is.EqualTo(1), "two calls, one edge"); + Assert.That(_dsmModel.GetRelationCount(provider, consumer), Is.EqualTo(0)); + }); + } + + [Test] + public void Build_MethodsAndFields_AreNotAddedAsElements() + { + var a = _graph.CreateClass("A"); + _graph.CreateMethod("A.M", a); + _graph.CreateField("A.F", a); + + Assert.That(Build(), Is.EqualTo(1)); + Assert.That(AddedElementCount(), Is.EqualTo(1), "only the type itself"); + } + + [Test] + public void Build_ExternalType_IsExcluded() + { + var a = _graph.CreateClass("A"); + var external = _graph.CreateExternalClass("Ext"); + Rel(a, external, RelationshipType.Uses); + + Assert.That(Build(), Is.EqualTo(1)); + Assert.That(_dsmModel.GetElementByFullname("Ext"), Is.Null); + } + + [Test] + public void Build_MutualDependency_KeepsBothDirections() + { + // The case the matrix paints black, so both directions have to survive. + var a = _graph.CreateClass("A"); + var b = _graph.CreateClass("B"); + Rel(a, b, RelationshipType.Uses); + Rel(b, a, RelationshipType.Uses); + + Build(); + + var elementA = _dsmModel.GetElementByFullname("A"); + var elementB = _dsmModel.GetElementByFullname("B"); + Assert.Multiple(() => + { + Assert.That(_dsmModel.GetRelationCount(elementA, elementB), Is.EqualTo(1)); + Assert.That(_dsmModel.GetRelationCount(elementB, elementA), Is.EqualTo(1)); + }); + } + + [Test] + public void Build_AcyclicChain_IsOrderedSoAllDependenciesSitOnOneSide() + { + // A -> B -> C, added in an order that is not the dependency order. Partitioning has to fix that, + // otherwise an acyclic structure does not look acyclic in the matrix. + var b = _graph.CreateClass("B"); + var c = _graph.CreateClass("C"); + var a = _graph.CreateClass("A"); + Rel(a, b, RelationshipType.Uses); + Rel(b, c, RelationshipType.Uses); + + Build(); + + // The matrix reads cell[row][column] as "column depends on row", so a provider must come after its + // consumer for the weight to land below the diagonal. + var order = _dsmModel.RootElement.Children.Select(e => e.Name).ToList(); + Assert.That(order.IndexOf("A"), Is.LessThan(order.IndexOf("B"))); + Assert.That(order.IndexOf("B"), Is.LessThan(order.IndexOf("C"))); + } + + [Test] + public void Build_SortsWithinAParentOnly_HierarchyIsPreserved() + { + var assembly = _graph.CreateAssembly("Asm"); + var ns = _graph.CreateNamespace("Ns", assembly); + var x = _graph.CreateClass("X", ns); + var y = _graph.CreateClass("Y", ns); + Rel(x, y, RelationshipType.Uses); + + Build(); + + var nsElement = _dsmModel.GetElementByFullname("Asm.Ns"); + Assert.That(nsElement.Children.Select(e => e.Name), Is.EquivalentTo(new[] { "X", "Y" }), + "partitioning must reorder siblings, never move them out of their parent"); + } + + [Test] + public void Build_NamespaceChainWithoutOwnTypes_IsMergedIntoOneLevel() + { + // The real shape: the parser makes one element per namespace segment, so an assembly whose root + // namespace repeats its own name produces a chain of levels that hold nothing. + var assembly = _graph.CreateAssembly("CSharpCodeAnalyst.CodeGraph"); + var outer = _graph.CreateNamespace("CSharpCodeAnalyst", assembly); + var inner = _graph.CreateNamespace("CodeGraph", outer); + var graphNs = _graph.CreateNamespace("Graph", inner); + var algorithms = _graph.CreateNamespace("Algorithms", inner); + _graph.CreateClass("CodeElement", graphNs); + _graph.CreateClass("TypeGraph", algorithms); + + Build(); + + // "CSharpCodeAnalyst" has a single namespace child and does not become a level of its own; "CodeGraph" + // branches and does. The merged label is the namespace that actually exists. + var assemblyElement = _dsmModel.GetElementByFullname("CSharpCodeAnalyst.CodeGraph"); + Assert.That(assemblyElement.Children.Select(e => e.Name), + Is.EquivalentTo(new[] { "CSharpCodeAnalyst.CodeGraph" }), + "the pass-through level must not sit between the assembly and the branching namespace"); + } + + [Test] + public void Build_ChainOfSeveralPassThroughs_MergesAllOfTheirNames() + { + // A -> B -> C where only C branches: one level named after all three. + var assembly = _graph.CreateAssembly("Asm"); + var a = _graph.CreateNamespace("A", assembly); + var b = _graph.CreateNamespace("B", a); + var c = _graph.CreateNamespace("C", b); + _graph.CreateClass("X", c); + _graph.CreateClass("Y", c); + + Build(); + + var assemblyElement = _dsmModel.GetElementByFullname("Asm"); + Assert.That(assemblyElement.Children.Select(e => e.Name), Is.EquivalentTo(new[] { "A.B.C" })); + } + + [Test] + public void Build_TypeUnderMergedNamespace_KeepsItsOwnName() + { + // Only namespaces are ever merged: the parent of a type holds a type, so it is not a pass-through. + var assembly = _graph.CreateAssembly("Asm"); + var a = _graph.CreateNamespace("A", assembly); + var b = _graph.CreateNamespace("B", a); + _graph.CreateClass("X", b); + _graph.CreateClass("Y", b); + + Build(); + + Assert.That(_dsmModel.GetElementByFullname("Asm.A.B.X"), Is.Not.Null); + } + + [Test] + public void Build_NamespaceWithSeveralChildren_IsKept() + { + var assembly = _graph.CreateAssembly("Asm"); + var ns = _graph.CreateNamespace("Ns", assembly); + _graph.CreateClass("A", ns); + _graph.CreateClass("B", ns); + + Build(); + + Assert.That(_dsmModel.GetElementByFullname("Asm.Ns"), Is.Not.Null); + } + + [Test] + public void Build_NamespaceWithASingleType_IsKept() + { + // One child, but it is a type: the namespace does carry content and has to stay. + var assembly = _graph.CreateAssembly("Asm"); + var ns = _graph.CreateNamespace("Ns", assembly); + _graph.CreateClass("Only", ns); + + Build(); + + Assert.That(_dsmModel.GetElementByFullname("Asm.Ns.Only"), Is.Not.Null); + } + + [Test] + public void Build_NamespaceBranchesOnlyIntoExcludedTypes_CountsAsPassThrough() + { + // Two children in the code graph, but one is external and never reaches the model. What matters is + // what the matrix ends up showing, so this is a pass-through despite the code graph branching. + var assembly = _graph.CreateAssembly("Asm"); + var outer = _graph.CreateNamespace("Outer", assembly); + var inner = _graph.CreateNamespace("Inner", outer); + _graph.CreateExternalClass("Ext", outer); + _graph.CreateClass("A", inner); + _graph.CreateClass("B", inner); + + Build(); + + var assemblyElement = _dsmModel.GetElementByFullname("Asm"); + Assert.That(assemblyElement.Children.Select(e => e.Name), Is.EquivalentTo(new[] { "Outer.Inner" })); + } + + [Test] + public void Build_CalledTwice_DoesNotAccumulate() + { + _graph.CreateClass("A"); + + Build(); + var countAfterFirst = AddedElementCount(); + Build(); + + Assert.That(AddedElementCount(), Is.EqualTo(countAfterFirst)); + } +} diff --git a/Tests/UnitTests/DsmMatrix/DsmMatrixFactoryTests.cs b/Tests/UnitTests/DsmMatrix/DsmMatrixFactoryTests.cs new file mode 100644 index 00000000..a1eca235 --- /dev/null +++ b/Tests/UnitTests/DsmMatrix/DsmMatrixFactoryTests.cs @@ -0,0 +1,58 @@ +using CodeParserTests.Helper; +using CSharpCodeAnalyst.CodeGraph.Graph; +using CSharpCodeAnalyst.Features.DsmMatrix; + +namespace CodeParserTests.UnitTests.DsmMatrix; + +[TestFixture] +public class DsmMatrixFactoryTests +{ + private static TestCodeGraph BuildGraph() + { + var graph = new TestCodeGraph(); + var assembly = graph.CreateAssembly("Asm"); + var ns = graph.CreateNamespace("Ns", assembly); + var a = graph.CreateClass("A", ns); + var b = graph.CreateClass("B", ns); + a.Relationships.Add(new Relationship(a.Id, b.Id, RelationshipType.Uses)); + return graph; + } + + [Test] + public void Create_BuildsAMatrixOverTheWholeGraph() + { + var viewModel = DsmMatrixFactory.Create(BuildGraph()); + + Assert.That(viewModel.ActiveMatrix, Is.Not.Null); + Assert.That(viewModel.ActiveMatrix.MatrixSize, Is.GreaterThan(0)); + } + + [Test] + public void Create_OnABackgroundThread_DoesNotNeedADispatcher() + { + // MainViewModel.OnShowDsm builds this inside Task.Run, because the work is quadratic in the number + // of types. That only holds as long as nothing in DsmSuite's view models has thread affinity, so + // pin it down: a plain worker thread with no Dispatcher of its own has to get through. + var graph = BuildGraph(); + Exception? failure = null; + object? result = null; + + var thread = new Thread(() => + { + try + { + result = DsmMatrixFactory.Create(graph); + } + catch (Exception ex) + { + failure = ex; + } + }); + thread.Start(); + var finished = thread.Join(TimeSpan.FromSeconds(30)); + + Assert.That(finished, Is.True, "building the matrix blocked on a background thread"); + Assert.That(failure, Is.Null, $"building the matrix threw: {failure}"); + Assert.That(result, Is.Not.Null); + } +} diff --git a/ThirdParty/DsmSuite/Directory.Build.props b/ThirdParty/DsmSuite/Directory.Build.props new file mode 100644 index 00000000..fb7196c4 --- /dev/null +++ b/ThirdParty/DsmSuite/Directory.Build.props @@ -0,0 +1,22 @@ + + + + + DsmSuite + + jmuijsenberg, ernstaii and contributors + Copyright (C) DsmSuite contributors. Licensed under GPL-3.0-or-later. + Vendored subset of DsmSuite (https://github.com/ernstaii/dsmsuite.sourcecode). + + diff --git a/ThirdParty/DsmSuite/Dsm.md b/ThirdParty/DsmSuite/Dsm.md new file mode 100644 index 00000000..82180856 --- /dev/null +++ b/ThirdParty/DsmSuite/Dsm.md @@ -0,0 +1,161 @@ +# Reading the DSM + +What the matrix on the **DSM** tab draws and how to read it. Almost none of this is discoverable from the +UI — the legend at the bottom covers three of the colours and nothing else — and the rest is spread over +`MatrixCellsView.OnRender`, `MatrixRowHeaderItemView.OnRender` and `MatrixViewModel.DefineCellColors`. + +For what we feed in, see `Features/DsmMatrix/CodeGraphToDsmModelBuilder.cs`; for our changes to the viewer +itself, see [README.md](README.md). + +## The axes + +> **Row = provider. Column = consumer.** A number at (row R, column C) means: **C depends on R.** + +```csharp +IDsmElement consumer = _elementViewModelLeafs[column].Element; +IDsmElement provider = _elementViewModelLeafs[row].Element; +int weight = _application.GetDependencyWeight(consumer, provider); +_cellWeights[row].Add(weight); +``` + +This is **not** the opposite of NDepend, which is the easy assumption to make. Their documentation says +verbatim: *"Blue cell means that the element in column uses the element in row"* — the same convention. What +NDepend adds is the mirror: *"Green cell means that the element in row uses the element in column"*, so every +dependency appears twice, blue below the diagonal and green above it, and a mutual pair is black in both. +That is why their layered example has "all blue cells in the lower-left triangle and all green cells in the +upper-right". + +Here each dependency is drawn **once**, so only one reading direction is populated: top to left. The mirror +would cost the cell colour, which is spent on nesting depth (see below) — NDepend spends it on direction and +has no depth-coloured blocks. + +### The order number + +Every row shows a number, and every column header repeats it. It is `IDsmElement.Order`, assigned 1..N +across the whole flattened tree — that is the cross reference between a column and its row. It is not +stable across runs: expanding or collapsing re-assigns it. + +## The diagonal and the darker squares + +Expanding an element paints the square block on the diagonal that spans all of its leaves in that element's +**nesting depth colour** (`MatrixColorConverter.GetColor(depth)`, our ramp in +`Features/DsmMatrix/DsmMatrixTheme.xaml`, darker with depth). The diagonal cell of each leaf gets the same +treatment. + +This is the single most useful reading aid in the whole view: + +> **Inside the square = internal to that assembly or namespace. Outside = crosses its boundary.** + +The block is drawn once from its top-left corner (`parent.Children[0] == child`); the root gets none +(`Depth > 0`). + +**There are only four depth colours, and they cycle:** + +```csharp +public static MatrixColor GetColor(int depth) +{ + switch (depth % 4) { ... } // Color1 .. Color4 +} +``` + +So the shade is not the depth, it is the depth modulo four — nest five levels deep and the innermost block +is painted like the outermost. That makes the ramp a scarce resource: every nesting level that carries no +structure of its own still burns one of the four, shifts the rest, and brings the wrap-around one level +closer. It is the main reason the builder drops pass-through namespaces (see below), and worth remembering +before adding a level to the hierarchy we feed in. + +## Cell colours + +| Colour | Meaning | +|---|---| +| Background (light neutral) | no relation, and not inside an expanded block | +| Depth ramp `MatrixColor1..4` | inside an expanded element's block, shade = nesting depth | +| `MatrixColorCycle` (warm orange) | **the two elements depend on each other** | + +The cycle colour overwrites everything else, which is why it is the loudest colour in our palette. + +Hovering or selecting a row/column multiplies the cell colour by 1.1 / 1.2 (`MatrixTheme.GetHighlightBrush`) +to draw the crosshair. + +## Inside a cell: the number + +The **number** is the dependency weight: the count of distinct type-level edges aggregated under the two +elements. Above `9999` it reads `>9K`, because that is the widest that fits — the exact value is in the +cell's tooltip. + +It is drawn smaller than the rest of the matrix (font size 10 against 14). That is not decoration: at 14 a +cell only has room for three digits, and upstream silently dropped the fourth, drawing `1000` as `100`. See +[README.md](README.md). + +> **Fully expanded, every populated cell reads `1`.** `TypeGraph` deduplicates, so there is exactly one +> edge per pair of types, and we write it with `weight: 1`. `DsmRelationModel.AddWeights` sums along both +> ancestor chains, so aggregation only happens *above* the leaves. + +Upstream also drew the weight as a small bar under the number, sized by its decile among all populated +cells. We removed it; see [README.md](README.md). + +## The colored bar beside the row headers + +The bar at the right edge of a row header, against the matrix, is **relative to the currently selected +row**. It answers "how does this row relate to the thing I clicked", not "what is this row". + +It has to be a **row**: `UpdateRelationFlags` reads `SelectedRow`, and `SelectColumn` sets `SelectedRow` to +null. Clicking a column header therefore draws the crosshair but clears the bars. With nothing selected +there are none at all. + +`MatrixRowHeaderItemView.GetIndicatorColor()`: + +| Colour | Flag | Meaning | +|---|---|---| +| Green | `IsConsumer` | this row **uses** the selected element | +| Blue | `IsProvider` | this row **is used by** the selected element | +| Orange | both | **mutual — a cycle** | + +Upstream also drew a second bar at the *left* edge, an addition of this fork, marking the leaves of an +expanded selection that had relations reaching outside it. We removed it; see [README.md](README.md). + +## What we put into it + +From `CodeGraphToDsmModelBuilder`: + +- **One vertex per internal type.** Fine-grained relationships (calls, field access, ...) are lifted to the + containing type and deduplicated; external types and self edges are dropped (`TypeGraph`). +- **The hierarchy comes from the code graph's parent chain**, not from splitting dotted names. Assembly + names keep their dots. +- **Pass-through namespaces are merged into one level** — a namespace whose only child is another namespace + holds no types of its own. The parser creates one element per namespace *segment*, so every project whose + root namespace repeats its assembly name grows such a chain: assembly `CSharpCodeAnalyst.CodeGraph` holds + namespace `CSharpCodeAnalyst` holds namespace `CodeGraph` holds the real ones. Without this, eight + assemblies gave eight rows all reading `CSharpCodeAnalyst`. Each level costs an expand that reveals a + single row carrying the same numbers as before, a vertical strip, and one of the only four depth colours. + + The chain becomes one element carrying all of their names, so the row reads `CSharpCodeAnalyst.CodeGraph` + — the namespace that exists — rather than `CodeGraph`, which names nothing. A label is always the namespace + path relative to the element it sits in: one segment where nothing was collapsed (`Algorithms` inside + `CSharpCodeAnalyst.CodeGraph`), the whole collapsed chain where something was. +- **Rows and columns are partitioned** (`PartitionSortAlgorithm`) so an acyclic structure actually comes out + triangular. Without it the order is meaningless and a layered design looks as tangled as a knot. +- **All weights are currently 1** per distinct type-level edge, not the number of underlying relationships. + The numbers you see on a collapsed element are sums of those. + +## The metrics panel is hidden + +DsmSuite has a metrics column between the row headers and the cells, reachable through the arrow in the top +left corner. Both are hidden — its numbers contradict the application's own system metrics. The reasoning is +in [README.md](README.md). + +## Controls + +- **Ctrl + mouse wheel** zooms the whole matrix (0.04 – 4.0). +- **Plain wheel** scrolls up and down, **shift + wheel** sideways — from anywhere in the matrix, the headers + included. Worth knowing because the scroll bars sit *inside* the scaled grid, so zooming out shrinks them + along with everything else until they are too thin to grab. The wheel is unaffected: it moves by a fixed + number of content units, which is the same couple of cells at any zoom. +- **Click a row header** to select it — this is what drives the indicator bars. Clicking a column header + selects the column but clears them. +- **Click the arrow** in a row header (the top-left 20×24 px) to expand or collapse; hold **shift** and it + works recursively. + +The view is read-only. Everything upstream offers that edits the DSM model — the context menus, dragging a +row header onto another to re-parent it — is removed, because the model here is a projection of a parsed +code graph and an edited row would no longer say anything about the code. See [README.md](README.md). diff --git a/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Core/DsiElement.cs b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Core/DsiElement.cs new file mode 100644 index 00000000..6e05e019 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Core/DsiElement.cs @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.Analyzer.Model.Interface; + +namespace DsmSuite.Analyzer.Model.Core +{ + /// + /// Represents element of a component. Both the ElementId and Name uniquely identify an element. + /// + public class DsiElement : IDsiElement + { + public DsiElement(int id, string name, string type, IDictionary properties) + { + Id = id; + Name = name; + Type = type; + Properties = properties; + } + + public int Id { get; } + public string Name { get; set; } + public string Type { get; } + public IDictionary Properties { get; } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Core/DsiElementModel.cs b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Core/DsiElementModel.cs new file mode 100644 index 00000000..4418a731 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Core/DsiElementModel.cs @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using System; +using System.Collections.Generic; +using DsmSuite.Analyzer.Model.Interface; +using DsmSuite.Analyzer.Model.Persistency; +using DsmSuite.Common.Util; + +namespace DsmSuite.Analyzer.Model.Core +{ + public class DsiElementModel : IDsiElementModelFileCallback + { + private readonly Dictionary _elementsByName; + private readonly Dictionary _elementsById; + private readonly Dictionary _elementTypeCount; + + public event EventHandler ElementRemoved; + + public DsiElementModel() + { + _elementsByName = new Dictionary(); + _elementsById = new Dictionary(); + _elementTypeCount = new Dictionary(); + } + + public void Clear() + { + _elementsByName.Clear(); + _elementsById.Clear(); + _elementTypeCount.Clear(); + } + + public IDsiElement ImportElement(int id, string name, string type, IDictionary properties) + { + Logger.LogDataModelMessage($"Import element id={id} name={name} type={type}"); + + DsiElement element = new DsiElement(id, name, type, properties); + string key = CreateKey(element.Name); + _elementsByName[key] = element; + _elementsById[element.Id] = element; + IncrementElementTypeCount(element.Type); + return element; + } + + public IDsiElement AddElement(string name, string type, IDictionary properties) + { + Logger.LogDataModelMessage($"Add element name={name} type={type}"); + + string key = CreateKey(name); + if (!_elementsByName.ContainsKey(key)) + { + IncrementElementTypeCount(type); + int id = _elementsByName.Count + 1; + DsiElement element = new DsiElement(id, name, type, properties); + _elementsByName[key] = element; + _elementsById[id] = element; + return element; + } + else + { + return null; + } + } + + public void IgnoreElement(string name, string type) + { + Logger.LogDataModelMessage($"Ignore element name={name} type={type}"); + } + + public void RemoveElement(IDsiElement element) + { + Logger.LogDataModelMessage($"Remove element id={element.Id} name={element.Name} type={element.Type}"); + + string key = element.Name.ToLower(); + _elementsByName.Remove(key); + _elementsById.Remove(element.Id); + + ElementRemoved?.Invoke(this, element.Id); + } + + public void RenameElement(IDsiElement element, string newName) + { + Logger.LogDataModelMessage("Rename element id={element.Id} from {element.Name} to {newName}"); + + DsiElement e = element as DsiElement; + if (e != null) + { + string oldKey = CreateKey(e.Name); + _elementsByName.Remove(oldKey); + e.Name = newName; + string newKey = CreateKey(e.Name); + _elementsByName[newKey] = e; + } + } + + public IDsiElement FindElementById(int id) + { + return _elementsById.ContainsKey(id) ? _elementsById[id] : null; + } + + public IDsiElement FindElementByName(string name) + { + string key = name.ToLower(); + return _elementsByName.ContainsKey(key) ? _elementsByName[key] : null; + } + + public IEnumerable GetElements() + { + return _elementsById.Values; + } + + public ICollection GetElementTypes() + { + return _elementTypeCount.Keys; + } + + public int GetElementTypeCount(string type) + { + if (_elementTypeCount.ContainsKey(type)) + { + return _elementTypeCount[type]; + } + else + { + return 0; + } + } + + public int CurrentElementCount => _elementsByName.Values.Count; + + private void IncrementElementTypeCount(string type) + { + if (!_elementTypeCount.ContainsKey(type)) + { + _elementTypeCount[type] = 0; + } + _elementTypeCount[type]++; + } + + private string CreateKey(string name) + { + return name.ToLower(); + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Core/DsiModel.cs b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Core/DsiModel.cs new file mode 100644 index 00000000..91df3409 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Core/DsiModel.cs @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text.RegularExpressions; +using DsmSuite.Analyzer.Model.Interface; +using DsmSuite.Analyzer.Model.Persistency; +using DsmSuite.Common.Model.Core; +using DsmSuite.Common.Model.Interface; +using DsmSuite.Common.Util; + +namespace DsmSuite.Analyzer.Model.Core +{ + public class DsiModel : IDsiModel + { + private readonly MetaDataModel _metaDataModel; + private readonly DsiElementModel _elementsDataModel; + private readonly DsiRelationModel _relationsDataModel; + private readonly List _ignoredNames; + + public DsiModel(string processStep, List ignoredNames, Assembly executingAssembly) + { + _metaDataModel = new MetaDataModel(processStep, executingAssembly); + _elementsDataModel = new DsiElementModel(); + _relationsDataModel = new DsiRelationModel(_elementsDataModel); + + _ignoredNames = ignoredNames; + } + + public string Filename { get; private set; } + + public void Load(string dsiFilename, IProgress progress) + { + Logger.LogDataModelMessage($"Load data model file={dsiFilename}"); + + Filename = dsiFilename; + DsiModelFile modelFile = + new DsiModelFile(dsiFilename, _metaDataModel, _elementsDataModel, _relationsDataModel); + modelFile.Load(progress); + } + + public void Save(string dsiFilename, bool compressFile, IProgress progress) + { + Logger.LogDataModelMessage($"Save data model file={dsiFilename} compress={compressFile}"); + + Filename = dsiFilename; + + foreach (string type in GetElementTypes()) + { + _metaDataModel.AddMetaDataItemToDefaultGroup($"- '{type}' elements found", + $"{GetElementTypeCount(type)}"); + } + + _metaDataModel.AddMetaDataItemToDefaultGroup("Total elements found", $"{CurrentElementCount}"); + + foreach (string type in GetRelationTypes()) + { + _metaDataModel.AddMetaDataItemToDefaultGroup($"- '{type}' relations found", + $"{GetRelationTypeCount(type)}"); + } + + _metaDataModel.AddMetaDataItemToDefaultGroup("Total relations found", $"{ImportedRelationCount}"); + _metaDataModel.AddMetaDataItemToDefaultGroup("Total relations resolved", + $"{ResolvedRelationCount} (confidence={ResolvedRelationPercentage:0.000} %)"); + + DsiModelFile modelFile = + new DsiModelFile(dsiFilename, _metaDataModel, _elementsDataModel, _relationsDataModel); + modelFile.Save(compressFile, progress); + } + + public void AddMetaData(string name, string value) + { + _metaDataModel.AddMetaDataItemToDefaultGroup(name, value); + } + + public IEnumerable GetMetaDataGroups() + { + return _metaDataModel.GetExportedMetaDataGroups(); + } + + public IEnumerable GetMetaDataGroupItems(string groupName) + { + return _metaDataModel.GetExportedMetaDataGroupItems(groupName); + } + + public IDsiElement AddElement(string name, string type, IDictionary properties) + { + IDsiElement element = null; + + if (!Ignore(name)) + { + element = _elementsDataModel.AddElement(name, type, properties); + } + else + { + _elementsDataModel.IgnoreElement(name, type); + } + + return element; + } + + public void RemoveElement(IDsiElement element) + { + _elementsDataModel.RemoveElement(element); + } + + public void RenameElement(IDsiElement element, string newName) + { + _elementsDataModel.RenameElement(element, newName); + } + + public IDsiElement FindElementById(int id) + { + return _elementsDataModel.FindElementById(id); + } + + public IDsiElement FindElementByName(string name) + { + return _elementsDataModel.FindElementByName(name); + } + + public IEnumerable GetElements() + { + return _elementsDataModel.GetElements(); + } + + public ICollection GetElementTypes() + { + return _elementsDataModel.GetElementTypes(); + } + + public int GetElementTypeCount(string type) + { + return _elementsDataModel.GetElementTypeCount(type); + } + + public int CurrentElementCount => _elementsDataModel.CurrentElementCount; + + public IDsiRelation AddRelation(string consumerName, string providerName, string type, int weight, IDictionary properties) + { + IDsiRelation relation = null; + + if (!Ignore(consumerName) && !Ignore(providerName)) + { + relation = _relationsDataModel.AddRelation(consumerName, providerName, type, weight, properties); + } + else + { + _relationsDataModel.IgnoreRelation(consumerName, providerName, type, weight); + } + + return relation; + } + + public void SkipRelation(string consumerName, string providerName, string type) + { + _relationsDataModel.SkipRelation(consumerName, providerName, type); + } + + public void AmbiguousRelation(string consumerName, string providerName, string type) + { + _relationsDataModel.AmbiguousRelation(consumerName, providerName, type); + } + + public ICollection GetRelationTypes() + { + return _relationsDataModel.GetRelationTypes(); + } + + public int GetRelationTypeCount(string type) + { + return _relationsDataModel.GetRelationTypeCount(type); + } + + public ICollection GetRelationsOfConsumer(int consumerId) + { + return _relationsDataModel.GetRelationsOfConsumer(consumerId); + } + + public IEnumerable GetRelations() + { + return _relationsDataModel.GetRelations(); + } + + public int CurrentRelationCount => _relationsDataModel.CurrentRelationCount; + + public bool DoesRelationExist(int consumerId, int providerId) + { + return _relationsDataModel.DoesRelationExist(consumerId, providerId); + } + + public int ImportedRelationCount => _relationsDataModel.ImportedRelationCount; + + public int ResolvedRelationCount => _relationsDataModel.ResolvedRelationCount; + + public double ResolvedRelationPercentage => _relationsDataModel.ResolvedRelationPercentage; + + public double AmbiguousRelationPercentage => _relationsDataModel.AmbiguousRelationPercentage; + + private bool Ignore(string name) + { + bool ignore = false; + + foreach (string ignoredName in _ignoredNames) + { + Regex regex = new Regex(ignoredName); + Match match = regex.Match(name); + if (match.Success) + { + ignore = true; + } + } + return ignore; + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Core/DsiRelation.cs b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Core/DsiRelation.cs new file mode 100644 index 00000000..4ad1f00e --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Core/DsiRelation.cs @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.Analyzer.Model.Interface; + +namespace DsmSuite.Analyzer.Model.Core +{ + /// + /// Represents a relation of a specific type between two elements + /// + public class DsiRelation : IDsiRelation + { + public DsiRelation(int consumerId, int providerId, string type, int weight, IDictionary properties) + { + ConsumerId = consumerId; + ProviderId = providerId; + Type = type; + Weight = weight; + Properties = properties; + } + + public int ConsumerId { get; } + public int ProviderId { get; } + public string Type { get; } + public int Weight { get; set; } + public IDictionary Properties { get; } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Core/DsiRelationModel.cs b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Core/DsiRelationModel.cs new file mode 100644 index 00000000..b599bb72 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Core/DsiRelationModel.cs @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.Analyzer.Model.Interface; +using DsmSuite.Analyzer.Model.Persistency; +using DsmSuite.Common.Util; + +namespace DsmSuite.Analyzer.Model.Core +{ + public class DsiRelationModel : IDsiRelationModelFileCallback + { + private readonly DsiElementModel _elementsDataModel; + private readonly Dictionary>>> _relationsByConsumerId; + private readonly Dictionary _relationTypeCount; + private int _importedRelationCount; + private int _resolvedRelationCount; + private int _ambiguousRelationCount; + + public DsiRelationModel(DsiElementModel elementsDataModel) + { + _elementsDataModel = elementsDataModel; + _elementsDataModel.ElementRemoved += OnElementRemoved; + _relationsByConsumerId = new Dictionary>>>(); + _relationTypeCount = new Dictionary(); + } + + public void Clear() + { + _relationsByConsumerId.Clear(); + _relationTypeCount.Clear(); + _importedRelationCount = 0; + _resolvedRelationCount = 0; + _ambiguousRelationCount = 0; + } + + public IDsiRelation ImportRelation(int consumerId, int providerId, string type, int weight, IDictionary properties) + { + Logger.LogDataModelMessage($"Import relation consumerId={consumerId} providerId={providerId} type={type} weight={weight}"); + + _importedRelationCount++; + + DsiRelation relation = null; + + IDsiElement consumer = _elementsDataModel.FindElementById(consumerId); + IDsiElement provider = _elementsDataModel.FindElementById(providerId); + + if ((consumer != null) && (provider != null)) + { + _resolvedRelationCount++; + relation = AddOrUpdateRelation(consumer.Id, provider.Id, type, weight, properties); + } + else + { + Logger.LogErrorDataModelRelationNotResolved(consumerId.ToString(), providerId.ToString()); + } + return relation; + } + + public IDsiRelation AddRelation(string consumerName, string providerName, string type, int weight, IDictionary properties) + { + Logger.LogDataModelMessage($"Add relation consumerName={consumerName} providerName={providerName} type={type} weight={weight}"); + + DsiRelation relation = null; + + _importedRelationCount++; + + IDsiElement consumer = _elementsDataModel.FindElementByName(consumerName); + IDsiElement provider = _elementsDataModel.FindElementByName(providerName); + + if ((consumer != null) && (provider != null)) + { + _resolvedRelationCount++; + relation = AddOrUpdateRelation(consumer.Id, provider.Id, type, weight, properties); + } + else + { + Logger.LogErrorDataModelRelationNotResolved(consumerName, providerName); + } + + return relation; + } + + public void IgnoreRelation(string consumerName, string providerName, string type, int weight) + { + Logger.LogDataModelMessage($"Ignore relation consumerName={consumerName} providerName={providerName} type={type} weight={weight}"); + } + + private DsiRelation AddOrUpdateRelation(int consumerId, int providerId, string type, int weight, IDictionary properties) + { + Dictionary> relations = GetRelations(consumerId, providerId); + + IncrementRelationTypeCount(type); + + if (!relations.ContainsKey(type)) + { + relations[type] = new List(); + } + + DsiRelation relation = new DsiRelation(consumerId, providerId, type, weight, properties); + relations[type].Add(relation); + return relation; + } + + public void IgnoreRelation(string consumerName, string providerName, string type) + { + Logger.LogDataModelMessage($"Ignore relation consumerName={consumerName} providerName={providerName} type={type}"); + } + + public void SkipRelation(string consumerName, string providerName, string type) + { + Logger.LogDataModelMessage($"Skip relation consumerName={consumerName} providerName={providerName} type={type}"); + + Logger.LogErrorDataModelRelationNotResolved(consumerName, providerName); + + _importedRelationCount++; + } + + public void AmbiguousRelation(string consumerName, string providerName, string type) + { + Logger.LogDataModelMessage($"Ambiguous relation consumerName={consumerName} providerName={providerName} type={type}"); + _ambiguousRelationCount++; + } + + public ICollection GetRelationTypes() + { + return _relationTypeCount.Keys; + } + + public int GetRelationTypeCount(string type) + { + if (_relationTypeCount.ContainsKey(type)) + { + return _relationTypeCount[type]; + } + else + { + return 0; + } + } + + public ICollection GetRelationsOfConsumer(int consumerId) + { + List consumerRelations = new List(); + if (_relationsByConsumerId.ContainsKey(consumerId)) + { + foreach (Dictionary> relationsForSpecificConsumer in _relationsByConsumerId[consumerId].Values) + { + foreach (List relation in relationsForSpecificConsumer.Values) + { + consumerRelations.AddRange(relation); + } + } + } + return consumerRelations; + } + + public IEnumerable GetRelations() + { + List allRelations = new List(); + foreach (Dictionary>> consumerRelations in _relationsByConsumerId.Values) + { + foreach (Dictionary> relationsForSpecificConsumer in consumerRelations.Values) + { + foreach (List relation in relationsForSpecificConsumer.Values) + { + allRelations.AddRange(relation); + } + } + } + return allRelations; + } + + public int CurrentRelationCount => GetRelations().Count(); + + public bool DoesRelationExist(int consumerId, int providerId) + { + return _relationsByConsumerId.ContainsKey(consumerId) && + _relationsByConsumerId[consumerId].ContainsKey(providerId); + } + + public int ImportedRelationCount => _importedRelationCount; + + public int ResolvedRelationCount => _resolvedRelationCount; + + public int AmbiguousRelationCount => _ambiguousRelationCount; + + public double ResolvedRelationPercentage + { + get + { + double resolvedRelationPercentage = 0.0; + if (ImportedRelationCount > 0) + { + resolvedRelationPercentage = (ResolvedRelationCount * 100.0) / ImportedRelationCount; + } + return resolvedRelationPercentage; + } + } + + public double AmbiguousRelationPercentage + { + get + { + double ambiguousRelationPercentage = 0.0; + if (ImportedRelationCount > 0) + { + ambiguousRelationPercentage = (AmbiguousRelationCount * 100.0) / ImportedRelationCount; + } + return ambiguousRelationPercentage; + } + } + + private void IncrementRelationTypeCount(string type) + { + if (!_relationTypeCount.ContainsKey(type)) + { + _relationTypeCount[type] = 0; + } + _relationTypeCount[type]++; + } + + private Dictionary> GetRelations(int consumerId, int providerId) + { + if (!_relationsByConsumerId.ContainsKey(consumerId)) + { + _relationsByConsumerId[consumerId] = new Dictionary>>(); + } + + if (!_relationsByConsumerId[consumerId].ContainsKey(providerId)) + { + _relationsByConsumerId[consumerId][providerId] = new Dictionary>(); + } + + return _relationsByConsumerId[consumerId][providerId]; + } + + private void OnElementRemoved(object sender, int elementId) + { + if (_relationsByConsumerId.ContainsKey(elementId)) + { + _relationsByConsumerId.Remove(elementId); + } + + foreach (var relationsByProviderId in _relationsByConsumerId.Values) + { + if (relationsByProviderId.ContainsKey(elementId)) + { + relationsByProviderId.Remove(elementId); + } + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/DsmSuite.Analyzer.Model.csproj b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/DsmSuite.Analyzer.Model.csproj new file mode 100644 index 00000000..348de515 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/DsmSuite.Analyzer.Model.csproj @@ -0,0 +1,14 @@ + + + + net10.0 + Library + enable + + + + + + + + diff --git a/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Interface/IDsiElement.cs b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Interface/IDsiElement.cs new file mode 100644 index 00000000..78451fd0 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Interface/IDsiElement.cs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +namespace DsmSuite.Analyzer.Model.Interface +{ + public interface IDsiElement + { + int Id { get; } + string Name { get; } + string Type { get; } + IDictionary Properties { get; } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Interface/IDsiModel.cs b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Interface/IDsiModel.cs new file mode 100644 index 00000000..bea37e0a --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Interface/IDsiModel.cs @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.Common.Model.Interface; +using DsmSuite.Common.Util; +using System; +using System.Collections.Generic; + +namespace DsmSuite.Analyzer.Model.Interface +{ + /// + /// Interface to the data model. An interface has been introduced to improve testability. + /// + public interface IDsiModel + { + string Filename { get; } + + // Model persistency + void Load(string dsiFilename, IProgress progress); + void Save(string dsiFilename, bool compressFile, IProgress progress); + + // Meta data + void AddMetaData(string name, string value); + IEnumerable GetMetaDataGroups(); + IEnumerable GetMetaDataGroupItems(string groupName); + + // Element editing + IDsiElement AddElement(string name, string type, IDictionary properties); + void RemoveElement(IDsiElement element); + void RenameElement(IDsiElement element, string newName); + + // Element queries + IDsiElement FindElementById(int id); + IDsiElement FindElementByName(string name); + IEnumerable GetElements(); + int CurrentElementCount { get; } + double ResolvedRelationPercentage { get; } + ICollection GetElementTypes(); + int GetElementTypeCount(string type); + + // Relation editing + IDsiRelation AddRelation(string consumerName, string providerName, string type, int weight, IDictionary properties); + void SkipRelation(string consumerName, string providerName, string type); + void AmbiguousRelation(string consumerName, string providerName, string type); + // Relation queries + ICollection GetRelationsOfConsumer(int consumerId); + IEnumerable GetRelations(); + int CurrentRelationCount { get; } + bool DoesRelationExist(int consumerId, int providerId); + int ImportedRelationCount { get; } + int ResolvedRelationCount { get; } + ICollection GetRelationTypes(); + int GetRelationTypeCount(string type); + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Interface/IDsiRelation.cs b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Interface/IDsiRelation.cs new file mode 100644 index 00000000..137b0ab8 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Interface/IDsiRelation.cs @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +namespace DsmSuite.Analyzer.Model.Interface +{ + public interface IDsiRelation + { + int ConsumerId { get; } + int ProviderId { get; } + string Type { get; } + int Weight { get; } + IDictionary Properties { get; } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Persistency/DsiModelFile.cs b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Persistency/DsiModelFile.cs new file mode 100644 index 00000000..37175793 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Persistency/DsiModelFile.cs @@ -0,0 +1,398 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using System; +using System.Collections.Generic; +using System.IO; +using System.Xml; +using DsmSuite.Analyzer.Model.Interface; +using DsmSuite.Common.Model.Interface; +using DsmSuite.Common.Model.Persistency; +using DsmSuite.Common.Util; + +namespace DsmSuite.Analyzer.Model.Persistency +{ + public class DsiModelFile + { + private const string RootXmlNode = "dsimodel"; + private const string ModelElementCountXmlAttribute = "elementCount"; + private const string ModelRelationCountXmlAttribute = "relationCount"; + + private const string MetaDataGroupXmlNode = "metadatagroup"; + private const string MetaDataGroupNameXmlAttribute = "name"; + private const string MetaDataXmlNode = "metadata"; + private const string MetaDataItemNameXmlAttribute = "name"; + private const string MetaDataItemValueXmlAttribute = "value"; + + private const string ElementGroupXmlNode = "elements"; + + private const string ElementXmlNode = "element"; + private const string ElementIdXmlAttribute = "id"; + private const string ElementNameXmlAttribute = "name"; + private const string ElementTypeXmlAttribute = "type"; + + private const string RelationGroupXmlNode = "relations"; + + private const string RelationXmlNode = "relation"; + private const string RelationFromXmlAttribute = "from"; + private const string RelationToXmlAttribute = "to"; + private const string RelationTypeXmlAttribute = "type"; + private const string RelationWeightXmlAttribute = "weight"; + + private readonly string _filename; + private readonly IMetaDataModelFileCallback _metaDataModelCallback; + private readonly IDsiElementModelFileCallback _elementModelCallback; + private readonly IDsiRelationModelFileCallback _relationModelCallback; + + private int _totalElementCount; + private int _progressedElementCount; + private int _totalRelationCount; + private int _progressedRelationCount; + private int _progressPercentage; + private string _progressActionText; + + public DsiModelFile(string filename, + IMetaDataModelFileCallback metaDataModelCallback, + IDsiElementModelFileCallback elementModelCallback, + IDsiRelationModelFileCallback relationModelCallback) + { + _filename = filename; + _metaDataModelCallback = metaDataModelCallback; + _elementModelCallback = elementModelCallback; + _relationModelCallback = relationModelCallback; + } + + public void Save(bool compressed, IProgress progress) + { + _progressActionText = "Saving dsi model"; + CompressedFile modelFile = new CompressedFile(_filename); + modelFile.WriteFile(WriteDsiXml, progress, compressed); + } + + public void Load(IProgress progress) + { + _progressActionText = "Loading dsi model"; + CompressedFile modelFile = new CompressedFile(_filename); + modelFile.ReadFile(ReadDsiXml, progress); + } + + private void WriteDsiXml(Stream stream, IProgress progress) + { + XmlWriterSettings settings = new XmlWriterSettings + { + Indent = true, + IndentChars = (" ") + }; + + using (XmlWriter writer = XmlWriter.Create(stream, settings)) + { + writer.WriteStartDocument(); + + writer.WriteStartElement(RootXmlNode); + { + WriteModelAttributes(writer); + WriteMetaData(writer); + WriteElements(writer, progress); + WriteRelations(writer, progress); + } + writer.WriteEndDocument(); + } + } + + private void ReadDsiXml(Stream stream, IProgress progress) + { + XmlReader xReader = XmlReader.Create(stream); + while (xReader.Read()) + { + switch (xReader.NodeType) + { + case XmlNodeType.Element: + ReadModelAttributes(xReader); + ReadMetaDataGroup(xReader); + ReadElement(xReader, progress); + ReadRelation(xReader, progress); + break; + case XmlNodeType.Text: + break; + case XmlNodeType.EndElement: + break; + } + } + } + + private void WriteModelAttributes(XmlWriter writer) + { + _totalElementCount = _elementModelCallback.CurrentElementCount; + writer.WriteAttributeString(ModelElementCountXmlAttribute, _totalElementCount.ToString()); + _progressedElementCount = 0; + + _totalRelationCount = _relationModelCallback.CurrentRelationCount; + writer.WriteAttributeString(ModelRelationCountXmlAttribute, _totalRelationCount.ToString()); + _progressedRelationCount = 0; + } + + private void ReadModelAttributes(XmlReader xReader) + { + if (xReader.Name == RootXmlNode) + { + int? elementCount = ParseInt(xReader.GetAttribute(ModelElementCountXmlAttribute)); + int? relationCount = ParseInt(xReader.GetAttribute(ModelRelationCountXmlAttribute)); + + if (elementCount.HasValue && relationCount.HasValue) + { + _totalElementCount = elementCount.Value; + _progressedElementCount = 0; + _totalRelationCount = relationCount.Value; + _progressedRelationCount = 0; + } + } + } + + private void WriteMetaData(XmlWriter writer) + { + foreach (string group in _metaDataModelCallback.GetExportedMetaDataGroups()) + { + WriteMetaDataGroup(writer, group); + } + } + + private void WriteMetaDataGroup(XmlWriter writer, string group) + { + writer.WriteStartElement(MetaDataGroupXmlNode); + writer.WriteAttributeString(MetaDataGroupNameXmlAttribute, group); + + foreach (IMetaDataItem metaDataItem in _metaDataModelCallback.GetExportedMetaDataGroupItems(group)) + { + writer.WriteStartElement(MetaDataXmlNode); + writer.WriteAttributeString(MetaDataItemNameXmlAttribute, metaDataItem.Name); + writer.WriteAttributeString(MetaDataItemValueXmlAttribute, metaDataItem.Value); + writer.WriteEndElement(); + } + writer.WriteEndElement(); + } + + private void ReadMetaDataGroup(XmlReader xReader) + { + if (xReader.Name == MetaDataGroupXmlNode) + { + string group = xReader.GetAttribute(MetaDataGroupNameXmlAttribute); + XmlReader xMetaDataReader = xReader.ReadSubtree(); + while (xMetaDataReader.Read()) + { + if (xMetaDataReader.Name == MetaDataXmlNode) + { + string name = xMetaDataReader.GetAttribute(MetaDataItemNameXmlAttribute); + string value = xMetaDataReader.GetAttribute(MetaDataItemValueXmlAttribute); + if ((name != null) && (value != null)) + { + _metaDataModelCallback.ImportMetaDataItem(group, name, value); + } + } + } + } + } + + private void WriteElements(XmlWriter writer, IProgress progress) + { + writer.WriteStartElement(ElementGroupXmlNode); + foreach (IDsiElement element in _elementModelCallback.GetElements()) + { + WriteElement(writer, element, progress); + } + writer.WriteEndElement(); + } + + private void WriteElement(XmlWriter writer, IDsiElement element, IProgress progress) + { + writer.WriteStartElement(ElementXmlNode); + writer.WriteAttributeString(ElementIdXmlAttribute, element.Id.ToString()); + writer.WriteAttributeString(ElementNameXmlAttribute, element.Name); + writer.WriteAttributeString(ElementTypeXmlAttribute, element.Type); + if (element.Properties != null) + { + foreach (KeyValuePair elementProperty in element.Properties) + { + writer.WriteAttributeString(elementProperty.Key, elementProperty.Value); + } + } + writer.WriteEndElement(); + + _progressedElementCount++; + UpdateProgress(progress); + } + + private void ReadElement(XmlReader xReader, IProgress progress) + { + if (xReader.Name == ElementXmlNode) + { + int? id = null; + string name = ""; + string type = ""; + + Dictionary elementProperties = new Dictionary(); + for (int attInd = 0; attInd < xReader.AttributeCount; attInd++) + { + xReader.MoveToAttribute(attInd); + switch (xReader.Name) + { + case ElementIdXmlAttribute: + id = ParseInt(xReader.Value); + break; + case ElementNameXmlAttribute: + name = xReader.Value; + break; + case ElementTypeXmlAttribute: + type = xReader.Value; + break; + default: + if (!string.IsNullOrEmpty(xReader.Value)) + { + elementProperties[xReader.Name] = xReader.Value; + } + break; + } + } + + if (id.HasValue) + { + if (elementProperties.Count > 0) + { + _elementModelCallback.ImportElement(id.Value, name, type, elementProperties); + } + else + { + _elementModelCallback.ImportElement(id.Value, name, type, null); + } + } + + _progressedElementCount++; + UpdateProgress(progress); + } + } + + private void WriteRelations(XmlWriter writer, IProgress progress) + { + writer.WriteStartElement(RelationGroupXmlNode); + foreach (IDsiRelation relation in _relationModelCallback.GetRelations()) + { + WriteRelation(writer, relation, progress); + } + writer.WriteEndElement(); + } + + private void WriteRelation(XmlWriter writer, IDsiRelation relation, IProgress progress) + { + writer.WriteStartElement(RelationXmlNode); + writer.WriteAttributeString(RelationFromXmlAttribute, relation.ConsumerId.ToString()); + writer.WriteAttributeString(RelationToXmlAttribute, relation.ProviderId.ToString()); + writer.WriteAttributeString(RelationTypeXmlAttribute, relation.Type); + writer.WriteAttributeString(RelationWeightXmlAttribute, relation.Weight.ToString()); + if (relation.Properties != null) + { + foreach (KeyValuePair relationProperty in relation.Properties) + { + writer.WriteAttributeString(relationProperty.Key, relationProperty.Value); + } + } + writer.WriteEndElement(); + + _progressedRelationCount++; + UpdateProgress(progress); + } + + private void ReadRelation(XmlReader xReader, IProgress progress) + { + if (xReader.Name == RelationXmlNode) + { + int? consumerId = null; + int? providerId = null; + string type = ""; + int? weight = null; + + Dictionary relationProperties = new Dictionary(); + for (int attInd = 0; attInd < xReader.AttributeCount; attInd++) + { + xReader.MoveToAttribute(attInd); + switch (xReader.Name) + { + case RelationFromXmlAttribute: + consumerId = ParseInt(xReader.Value); + break; + case RelationToXmlAttribute: + providerId = ParseInt(xReader.Value); + break; + case RelationTypeXmlAttribute: + type = xReader.Value; + break; + case RelationWeightXmlAttribute: + weight = ParseInt(xReader.Value); + break; + default: + if (!string.IsNullOrEmpty(xReader.Value)) + { + relationProperties[xReader.Name] = xReader.Value; + } + break; + } + } + + if (consumerId.HasValue && providerId.HasValue && weight.HasValue) + { + if (relationProperties.Count > 0) + { + _relationModelCallback.ImportRelation(consumerId.Value, providerId.Value, type, weight.Value, relationProperties); + } + else + { + _relationModelCallback.ImportRelation(consumerId.Value, providerId.Value, type, weight.Value, null); + } + } + + _progressedRelationCount++; + UpdateProgress(progress); + } + } + + private void UpdateProgress(IProgress progress) + { + if (progress != null) + { + int totalItemCount = _totalElementCount + _totalRelationCount; + int progressedItemCount = _progressedElementCount + _progressedRelationCount; + + int currentProgressPercentage = 0; + if (totalItemCount > 0) + { + currentProgressPercentage = progressedItemCount * 100 / totalItemCount; + } + + if (_progressPercentage != currentProgressPercentage) + { + _progressPercentage = currentProgressPercentage; + + ProgressInfo progressInfoInfo = new ProgressInfo + { + ActionText = _progressActionText, + Percentage = currentProgressPercentage, + TotalItemCount = totalItemCount, + CurrentItemCount = progressedItemCount, + ItemType = "items", + Done = totalItemCount == progressedItemCount + }; + + progress.Report(progressInfoInfo); + } + } + } + + private int? ParseInt(string value) + { + int? result = null; + + int parsedValued; + if (int.TryParse(value, out parsedValued)) + { + result = parsedValued; + } + return result; + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Persistency/IDsiElementModelFileCallback.cs b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Persistency/IDsiElementModelFileCallback.cs new file mode 100644 index 00000000..fa58c0b9 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Persistency/IDsiElementModelFileCallback.cs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.Analyzer.Model.Interface; +using System.Collections.Generic; + +namespace DsmSuite.Analyzer.Model.Persistency +{ + public interface IDsiElementModelFileCallback + { + IDsiElement ImportElement(int id, string name, string type, IDictionary properties); + IEnumerable GetElements(); + int CurrentElementCount { get; } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Persistency/IDsiRelationModelFileCallback.cs b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Persistency/IDsiRelationModelFileCallback.cs new file mode 100644 index 00000000..b8436bd3 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Analyzer.Model/Persistency/IDsiRelationModelFileCallback.cs @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.Analyzer.Model.Interface; + +namespace DsmSuite.Analyzer.Model.Persistency +{ + public interface IDsiRelationModelFileCallback + { + IDsiRelation ImportRelation(int consumerId, int providerId, string type, int weight, IDictionary properties); + IEnumerable GetRelations(); + int CurrentRelationCount { get; } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.Common.Model/Core/MetaDataItem.cs b/ThirdParty/DsmSuite/DsmSuite.Common.Model/Core/MetaDataItem.cs new file mode 100644 index 00000000..7e850672 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Common.Model/Core/MetaDataItem.cs @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.Common.Model.Interface; + +namespace DsmSuite.Common.Model.Core +{ + public class MetaDataItem : IMetaDataItem + { + public MetaDataItem(string name, string value) + { + Name = name; + Value = value; + } + + public string Name { get; } + public string Value { get; set; } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.Common.Model/Core/MetaDataModel.cs b/ThirdParty/DsmSuite/DsmSuite.Common.Model/Core/MetaDataModel.cs new file mode 100644 index 00000000..869cf4b3 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Common.Model/Core/MetaDataModel.cs @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using DsmSuite.Common.Model.Interface; +using DsmSuite.Common.Model.Persistency; +using DsmSuite.Common.Util; + +namespace DsmSuite.Common.Model.Core +{ + public class MetaDataModel : IMetaDataModelFileCallback + { + private readonly string _defaultGroupName; + private readonly Assembly _executingAssembly; + private readonly List _metaDataGroupNames; + private readonly Dictionary> _metaDataGroups; + + public MetaDataModel(string defaultGroupName, Assembly executingAssembly) + { + _defaultGroupName = defaultGroupName; + _executingAssembly = executingAssembly; + + _metaDataGroupNames = new List(); + _metaDataGroups = new Dictionary>(); + + AddDefaultItems(); + } + + public void Clear() + { + _metaDataGroupNames.Clear(); + _metaDataGroups.Clear(); + + AddDefaultItems(); + } + + public IMetaDataItem AddMetaDataItemToDefaultGroup(string name, string value) + { + return AddMetaDataItem(_defaultGroupName, name, value); + } + + public IMetaDataItem AddMetaDataItem(string groupName, string name, string value) + { + Logger.LogDataModelMessage($"Add metadata group={groupName} name={name} value={value}"); + + MetaDataItem item = FindItem(groupName, name); + if (item != null) + { + item.Value = value; + } + else + { + item = new MetaDataItem(name, value); + GetMetaDataGroupItemList(groupName).Add(item); + } + return item; + } + + public IEnumerable GetExportedMetaDataGroups() + { + return _metaDataGroupNames; + } + + public IEnumerable GetExportedMetaDataGroupItems(string groupName) + { + return GetMetaDataGroupItemList(groupName); + } + + private IList GetMetaDataGroupItemList(string groupName) + { + if (!_metaDataGroups.ContainsKey(groupName)) + { + _metaDataGroupNames.Add(groupName); + _metaDataGroups[groupName] = new List(); + } + + return _metaDataGroups[groupName]; + } + + private MetaDataItem FindItem(string groupName, string name) + { + return (from item in GetMetaDataGroupItemList(groupName) + where item.Name == name + select item).FirstOrDefault(); + } + + private void AddDefaultItems() + { + AddMetaDataItemToDefaultGroup("Executable", SystemInfo.GetExecutableInfo(_executingAssembly)); + } + + public IMetaDataItem ImportMetaDataItem(string groupName, string name, string value) + { + Logger.LogDataModelMessage($"Import metadata group={groupName} name={name} value={value}"); + + MetaDataItem item = FindItem(groupName, name); + if (item != null) + { + item.Value = value; + } + else + { + item = new MetaDataItem(name, value); + GetMetaDataGroupItemList(groupName).Add(item); + } + return item; + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.Common.Model/DsmSuite.Common.Model.csproj b/ThirdParty/DsmSuite/DsmSuite.Common.Model/DsmSuite.Common.Model.csproj new file mode 100644 index 00000000..f8a14986 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Common.Model/DsmSuite.Common.Model.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + Library + enable + + + + + + + \ No newline at end of file diff --git a/ThirdParty/DsmSuite/DsmSuite.Common.Model/Interface/IMetaDataItem.cs b/ThirdParty/DsmSuite/DsmSuite.Common.Model/Interface/IMetaDataItem.cs new file mode 100644 index 00000000..eadbaadc --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Common.Model/Interface/IMetaDataItem.cs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +namespace DsmSuite.Common.Model.Interface +{ + public interface IMetaDataItem + { + string Name { get; } + string Value { get; } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.Common.Model/Persistency/IMetaDataModelFileCallback.cs b/ThirdParty/DsmSuite/DsmSuite.Common.Model/Persistency/IMetaDataModelFileCallback.cs new file mode 100644 index 00000000..7aa1b886 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Common.Model/Persistency/IMetaDataModelFileCallback.cs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using System.Collections.Generic; +using DsmSuite.Common.Model.Interface; + +namespace DsmSuite.Common.Model.Persistency +{ + public interface IMetaDataModelFileCallback + { + IMetaDataItem ImportMetaDataItem(string group, string name, string value); + + IEnumerable GetExportedMetaDataGroups(); + IEnumerable GetExportedMetaDataGroupItems(string group); + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.Common.Util/CompressedFile.cs b/ThirdParty/DsmSuite/DsmSuite.Common.Util/CompressedFile.cs new file mode 100644 index 00000000..c6b7feea --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Common.Util/CompressedFile.cs @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using System; +using System.IO; +using System.IO.Compression; + +namespace DsmSuite.Common.Util +{ + /// + public class CompressedFile + { + private readonly string _filename; + private const int ZipLeadBytes = 0x04034b50; + + public delegate void ReadContent(Stream stream, IProgress progress); + public delegate void WriteContent(Stream stream, IProgress progress); + + public CompressedFile(string filename) + { + _filename = filename; + } + + public void ReadFile(ReadContent readContent, IProgress progress) + { + FileInfo fileInfo = new FileInfo(_filename); + if (fileInfo.Exists) + { + if (IsCompressed) + { + using (ZipArchive archive = ZipFile.OpenRead(fileInfo.FullName)) + { + if (archive.Entries.Count == 1) + { + using (Stream entryStream = archive.Entries[0].Open()) + { + readContent(entryStream, progress); + } + } + } + } + else + { + using (FileStream stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read)) + { + readContent(stream, progress); + } + } + } + } + + public void WriteFile(WriteContent writeContent, IProgress progress, bool compressed) + { + FileInfo fileInfo = new FileInfo(_filename); + if (compressed) + { + using (FileStream fileStream = new FileStream(fileInfo.FullName, FileMode.Create)) + { + using (ZipArchive archive = new ZipArchive(fileStream, ZipArchiveMode.Create, false)) + { + ZipArchiveEntry entry = archive.CreateEntry(fileInfo.Name); + using (Stream entryStream = entry.Open()) + { + writeContent(entryStream, progress); + } + } + } + } + else + { + using (FileStream fileStream = new FileStream(fileInfo.FullName, FileMode.Create)) + { + writeContent(fileStream, progress); + } + } + } + + public bool FileExists + { + get + { + FileInfo fileInfo = new FileInfo(_filename); + return fileInfo.Exists; + } + } + + public bool IsCompressed + { + get + { + bool isCompressedFile = false; + + FileInfo fileInfo = new FileInfo(_filename); + + if (fileInfo.Exists) + { + using (FileStream stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read)) + { + byte[] bytes = new byte[4]; + stream.Read(bytes, 0, 4); + if (BitConverter.ToInt32(bytes, 0) == ZipLeadBytes) + { + isCompressedFile = true; + } + } + } + + return isCompressedFile; + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.Common.Util/ConsoleActionBase.cs b/ThirdParty/DsmSuite/DsmSuite.Common.Util/ConsoleActionBase.cs new file mode 100644 index 00000000..10ddb122 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Common.Util/ConsoleActionBase.cs @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using System; +using System.Diagnostics; +using System.Text; + +namespace DsmSuite.Common.Util +{ + public abstract class ConsoleActionBase : IProgress + { + private const string InputParameters = "Input parameters"; + private const string Processing = "Processing"; + private const string OutputParameters = "Ouput parameters"; + private const string Performance = "Performance"; + + private readonly string _title; + private readonly Stopwatch _stopWatch; + private string _currentText = string.Empty; + + protected ConsoleActionBase(string title) + { + _title = title; + _stopWatch = new Stopwatch(); + } + + public void Execute() + { + LogTitle(); + EmptyLine(); + + Logger.LogAssemblyInfo(); + EmptyLine(); + + if (CheckPrecondition()) + { + StartTimer(); + + LogSubTitle(InputParameters); + LogInputParameters(); + EmptyLine(); + + LogSubTitle(Processing); + Action(); + EmptyLine(); + + LogSubTitle(OutputParameters); + LogOutputParameters(); + EmptyLine(); + + LogSubTitle(Performance); + StopTimer(); + Logger.LogUserMessage($"Total elapsed time={_stopWatch.Elapsed}"); + Logger.LogResourceUsage(); + EmptyLine(); + Logger.LogLogDirectory(); + } + } + + public void Report(ProgressInfo progress) + { + WriteToConsole( + progress.Percentage.HasValue + ? $"{progress.ActionText} {progress.CurrentItemCount}/{progress.TotalItemCount} {progress.ItemType} {progress.Percentage.Value}%" + : $"{progress.ActionText} {progress.CurrentItemCount} {progress.ItemType}", true, progress.Done); + } + + protected abstract bool CheckPrecondition(); + protected abstract void LogInputParameters(); + protected abstract void Action(); + protected abstract void LogOutputParameters(); + + private void StartTimer() + { + _stopWatch.Start(); + } + + private void StopTimer() + { + _stopWatch.Stop(); + } + + private void LogTitle() + { + Logger.LogUserMessage(_title); + Underline('=', _title.Length); + } + + private void LogSubTitle(string text) + { + Logger.LogUserMessage(text); + Underline('-', text.Length); + } + + private void Underline(char character, int length) + { + Logger.LogUserMessage(new String(character, length)); + } + + private void EmptyLine() + { + Logger.LogUserMessage(""); + } + + private void WriteToConsole(string text, bool overwrite, bool endOfLine) + { + if (!Console.IsOutputRedirected) + { + StringBuilder outputBuilder = new StringBuilder(); + + if (overwrite) + { + outputBuilder.Append("\r"); + outputBuilder.Append(text); + int overlapCount = _currentText.Length - text.Length; + if (overlapCount > 0) + { + outputBuilder.Append(' ', overlapCount); + outputBuilder.Append('\b', overlapCount); + } + + Console.Write(outputBuilder); + } + + outputBuilder.Clear(); + + if (overwrite) + { + outputBuilder.Append("\r"); + } + outputBuilder.Append(text); + Console.Write(outputBuilder); + + if (endOfLine) + { + outputBuilder.Append("\n"); + Logger.LogUserMessage(outputBuilder.ToString()); + } + + _currentText = outputBuilder.ToString(); + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.Common.Util/DsmSuite.Common.Util.csproj b/ThirdParty/DsmSuite/DsmSuite.Common.Util/DsmSuite.Common.Util.csproj new file mode 100644 index 00000000..aeeea636 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Common.Util/DsmSuite.Common.Util.csproj @@ -0,0 +1,18 @@ + + + net10.0 + Library + enable + + + + github + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/ThirdParty/DsmSuite/DsmSuite.Common.Util/ElementName.cs b/ThirdParty/DsmSuite/DsmSuite.Common.Util/ElementName.cs new file mode 100644 index 00000000..c2f36604 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Common.Util/ElementName.cs @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +namespace DsmSuite.Common.Util +{ + + public class ElementName + { + /// + /// Periods are used as path separator. This is the suggested character to use as a + /// non-path separating period. + /// + public const char NOPERIOD = '\u00b7'; // unicode middle dot + + public ElementName() + { + FullName = ""; + } + + public ElementName(string fullname) + { + FullName = fullname; + } + + public ElementName(string parentName, string name) + { + FullName = parentName; + AddNamePart(name); + } + + public void AddNamePart(string name) + { + if (FullName.Length > 0) + { + FullName += "."; + } + FullName += name; + } + + public string FullName { get; private set; } + + public string[] NameParts => FullName.Split('.'); + + public int NamePartCount => NameParts.Length; + + public string LastNamePart => NameParts[NameParts.Length - 1]; + + public string ParentName + { + get + { + string parentName = ""; + int beginIndex = 0; + int endIndex = FullName.Length - LastNamePart.Length - 1; + if (endIndex > 0) + { + parentName = FullName.Substring(beginIndex, endIndex); + } + return parentName; + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.Common.Util/FilePath.cs b/ThirdParty/DsmSuite/DsmSuite.Common.Util/FilePath.cs new file mode 100644 index 00000000..5571517a --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Common.Util/FilePath.cs @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using System.IO; +using System.Collections.Generic; + +namespace DsmSuite.Common.Util +{ + public static class FilePath + { + public static string ResolveFile(string path, string filename) + { + return Resolve(path, filename); + } + + public static List ResolveFiles(string path, IEnumerable filenames) + { + List resolvedFiles = new List(); + foreach (string filename in filenames) + { + resolvedFiles.Add(Resolve(path, filename)); + } + + return resolvedFiles; + } + + private static string Resolve_old(string path, string filename) + { + string absoluteFilename = ""; + string result = ""; + string[] assemblyFolders = filename.Split(';'); + int size = assemblyFolders.Length; + foreach (string folder in assemblyFolders) + { + absoluteFilename = Path.GetFullPath(folder); + if (!File.Exists(absoluteFilename)) + { + absoluteFilename = Path.GetFullPath(Path.Combine(path, folder)); + } + result += absoluteFilename + ";"; + Logger.LogInfo("Resolve file: path=" + path + " file=" + filename + " as file=" + absoluteFilename); + } + if (result.Length > 0) + { + result = result.Substring(0, result.Length - 1); + } + return result; + } + + private static string Resolve(string path, string filename) + { + string absoluteFilename = Path.GetFullPath(filename); + if (!File.Exists(absoluteFilename)) + { + absoluteFilename = Path.GetFullPath(Path.Combine(path ?? "", filename)); + } + + Logger.LogInfo("Resolve file: path=" + path + " file=" + filename + " as file=" + absoluteFilename); + return absoluteFilename; + } + } +} \ No newline at end of file diff --git a/ThirdParty/DsmSuite/DsmSuite.Common.Util/LogLevel.cs b/ThirdParty/DsmSuite/DsmSuite.Common.Util/LogLevel.cs new file mode 100644 index 00000000..e8260bae --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Common.Util/LogLevel.cs @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using System; + +namespace DsmSuite.Common.Util +{ + [Serializable] + public enum LogLevel + { + None, + User, + Error, + Warning, + Info, + Data, + All + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.Common.Util/Logger.cs b/ThirdParty/DsmSuite/DsmSuite.Common.Util/Logger.cs new file mode 100644 index 00000000..67197e61 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Common.Util/Logger.cs @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace DsmSuite.Common.Util +{ + /// + /// Provides logging to be used for diagnostic purposes + /// + public class Logger + { + private static Assembly _assembly; + private static string _logPath; + private static readonly Dictionary> DataModelRelationNotResolvedLogMessages; + + public static DirectoryInfo LogDirectory { get; private set; } + + static Logger() + { + DataModelRelationNotResolvedLogMessages = new Dictionary>(); + } + + public static void Init(Assembly assembly, bool logInCurrentDirectory) + { + _assembly = assembly; + _logPath = logInCurrentDirectory ? Directory.GetCurrentDirectory() : System.IO.Path.GetTempPath() + @"DsmSuiteLogging\"; + + LogLevel = LogLevel.None; + } + + public static LogLevel LogLevel { get; set; } + + + + public static void LogResourceUsage() + { + Process currentProcess = Process.GetCurrentProcess(); + const long million = 1000000; + long peakPagedMemMb = currentProcess.PeakPagedMemorySize64 / million; + long peakVirtualMemMb = currentProcess.PeakVirtualMemorySize64 / million; + long peakWorkingSetMb = currentProcess.PeakWorkingSet64 / million; + LogUserMessage($"Peak physical memory usage {peakWorkingSetMb:0.000}MB"); + LogUserMessage($"Peak paged memory usage {peakPagedMemMb:0.000}MB"); + LogUserMessage($"Peak virtual memory usage {peakVirtualMemMb:0.000}MB"); + } + + public static void LogAssemblyInfo() + { + string name = _assembly.GetName().Name; + string version = _assembly.GetName().Version.ToString(); + DateTime buildDate = new FileInfo(_assembly.Location).LastWriteTime; + LogUserMessage($"{SystemInfo.VersionLong}"); + LogUserMessage("Assembly: " + name + " version =" + version + " built on " + buildDate); + } + + public static void LogLogDirectory() { + LogUserMessage($"Log level {LogLevel}, location: {LogDirectory?.FullName}"); + } + + public static void LogUserMessage(string message, + [CallerFilePath] string sourceFile = "", + [CallerMemberName] string method = "", + [CallerLineNumber] int lineNumber = 0) + { + Console.WriteLine(message); + LogToFile(LogLevel.User, "userMessages.log", message); + } + + public static void LogError(string message, + [CallerFilePath] string sourceFile = "", + [CallerMemberName] string method = "", + [CallerLineNumber] int lineNumber = 0) + { + LogToFile(LogLevel.Error, "errorMessages.log", FormatLine(sourceFile, method, lineNumber, "error", message)); + } + + public static void LogException(string message, Exception e, + [CallerFilePath] string sourceFile = "", + [CallerMemberName] string method = "", + [CallerLineNumber] int lineNumber = 0) + { + LogToFile(LogLevel.Error, "exceptions.log", FormatLine(sourceFile, method, lineNumber, message, e.Message)); + LogToFile(LogLevel.Error, "exceptions.log", e.StackTrace); + LogToFile(LogLevel.Error, "exceptions.log", ""); + } + + public static void LogWarning(string message, + [CallerFilePath] string sourceFile = "", + [CallerMemberName] string method = "", + [CallerLineNumber] int lineNumber = 0) + { + LogToFile(LogLevel.Warning, "warningMessages.log", FormatLine(sourceFile, method, lineNumber, "error", message)); + } + + public static void LogInfo(string message, + [CallerFilePath] string sourceFile = "", + [CallerMemberName] string method = "", + [CallerLineNumber] int lineNumber = 0) + { + LogToFile(LogLevel.Info, "infoMessages.log", FormatLine(sourceFile, method, lineNumber, "info", message)); + } + + public static void LogDataModelMessage(string message, + [CallerFilePath] string sourceFile = "", + [CallerMemberName] string method = "", + [CallerLineNumber] int lineNumber = 0) + { + LogToFile(LogLevel.Data, "dataModelMessages.log", FormatLine(sourceFile, method, lineNumber, "info", message)); + } + + public static void LogErrorDataModelRelationNotResolved(string consumerName, string providerName) + { + string key = providerName; + string message = " From " + consumerName; + if (!DataModelRelationNotResolvedLogMessages.ContainsKey(key)) + { + DataModelRelationNotResolvedLogMessages[key] = new HashSet(); + } + DataModelRelationNotResolvedLogMessages[key].Add(message); + } + + public static void Flush() + { + Flush(LogLevel.Error, DataModelRelationNotResolvedLogMessages, "Relations not resolved", "dataModelRelationsNotResolved", 0); + } + + public static void LogToFile(LogLevel logLevel, string logFilename, string line) + { + if (LogLevel >= logLevel) + { + string path = GetLogFullPath(logFilename); + FileStream fs = new FileStream(path, FileMode.Append, FileAccess.Write); + using (StreamWriter writer = new StreamWriter(fs)) + { + writer.WriteLine(line); + } + } + } + + private static void Flush(LogLevel logLevel, Dictionary> messages, string title, string filename, int minCount) + { + string overviewFilename = filename + "Overview.txt"; + string detailsFilename = filename + "Details.txt"; + + int totalOccurrences = 0; + + List keys = messages.Keys.ToList(); + keys.Sort(); + + if (keys.Count > 0) + { + Logger.LogToFile(logLevel, overviewFilename, title); + Logger.LogToFile(logLevel, detailsFilename, title); + + Logger.LogToFile(logLevel, overviewFilename, "--------------------------------------------"); + Logger.LogToFile(logLevel, detailsFilename, "---------------------------------------------"); + } + + foreach (string key in keys) + { + int occurrences = messages[key].Count; + + if (occurrences > minCount) + { + totalOccurrences += occurrences; + Logger.LogToFile(logLevel, overviewFilename, $"{key} {occurrences} occurrences"); + Logger.LogToFile(logLevel, detailsFilename, $"{key} {occurrences} occurrences"); + foreach (string message in messages[key]) + { + Logger.LogToFile(logLevel, detailsFilename, " " + message); + } + } + } + + if (keys.Count > 0) + { + Logger.LogToFile(logLevel, overviewFilename, $"{keys.Count} items found in {totalOccurrences} occurrences"); + Logger.LogToFile(logLevel, detailsFilename, $"{keys.Count} items found in {totalOccurrences} occurrences"); + } + } + + private static DirectoryInfo CreateLogDirectory() + { + DateTime now = DateTime.Now; + string timestamp = $"{now.Year:0000}-{now.Month:00}-{now.Day:00}-{now.Hour:00}-{now.Minute:00}-{now.Second:00}"; + string assemblyName = _assembly.GetName().Name; + return Directory.CreateDirectory($@"{_logPath}\{assemblyName}_{timestamp}\"); + } + + private static string GetLogFullPath(string logFilename) + { + if (LogDirectory == null) + { + LogDirectory = CreateLogDirectory(); + } + + return Path.GetFullPath(Path.Combine(LogDirectory.FullName, logFilename)); + } + + private static string FormatLine(string sourceFile, string method, int lineNumber, string category, string text) + { + return StripPath(sourceFile) + " " + method + "() line=" + lineNumber + " " + category + "=" + text; + } + + private static string StripPath(string sourceFile) + { + char[] separators = { '\\' }; + string[] parts = sourceFile.Split(separators); + return parts[parts.Length - 1]; + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.Common.Util/ProgressInfo.cs b/ThirdParty/DsmSuite/DsmSuite.Common.Util/ProgressInfo.cs new file mode 100644 index 00000000..89d99cb9 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Common.Util/ProgressInfo.cs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +namespace DsmSuite.Common.Util +{ + public class ProgressInfo + { + public string ActionText; + public int TotalItemCount; + public int CurrentItemCount; + public string ItemType; + public int? Percentage; + public bool Done; + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.Common.Util/StringExtensions.cs b/ThirdParty/DsmSuite/DsmSuite.Common.Util/StringExtensions.cs new file mode 100644 index 00000000..5081e9ce --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Common.Util/StringExtensions.cs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using System.Text.RegularExpressions; + +namespace DsmSuite.Common.Util +{ + public static class StringExtensions + { + public static string ReplaceIgnoreCase(this string input, string oldValue, string newValue) + { + return Regex.Replace(input, Regex.Escape(oldValue), newValue.Replace("$", "$$"), RegexOptions.IgnoreCase); + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.Common.Util/SystemInfo.cs b/ThirdParty/DsmSuite/DsmSuite.Common.Util/SystemInfo.cs new file mode 100644 index 00000000..76fafd0c --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.Common.Util/SystemInfo.cs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using System; +using System.IO; +using System.Reflection; + +namespace DsmSuite.Common.Util +{ + public class SystemInfo + { + public static string GetExecutableInfo(Assembly assembly) + { + string name = assembly.GetName().Name; + string version = assembly.GetName().Version.ToString(); + DateTime buildDate = new FileInfo(assembly.Location).LastWriteTime; + return $"{name} version={version} build={buildDate}"; + } + + private static string Changes => ThisAssembly.Git.Commits != "0" ? $"-{ThisAssembly.Git.Commits}" : ""; + public static string Version => $"{ThisAssembly.Git.BaseTag}{Changes} {ThisAssembly.Git.Commit}"; + public static string VersionLong => $"DsmSuite version {Version}"; + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Base/ActionAttributes.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Base/ActionAttributes.cs new file mode 100644 index 00000000..0fb96598 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Base/ActionAttributes.cs @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using System; +using System.Collections.Generic; +using System.Text; + +namespace DsmSuite.DsmViewer.Application.Actions.Base +{ + public class ActionAttributes + { + readonly Dictionary _data; + + public ActionAttributes() + { + _data = new Dictionary(); + } + + public void SetString(string memberName, string memberValue) + { + _data[RemoveUnderscore(memberName)] = memberValue; + } + + public void SetInt(string memberName, int memberValue) + { + _data[RemoveUnderscore(memberName)] = memberValue.ToString(); + } + + public void SetNullableInt(string memberName, int? memberValue) + { + if (memberValue.HasValue) + { + _data[RemoveUnderscore(memberName)] = memberValue.Value.ToString(); + } + } + + public void SetListInt(string memberName, List list) + { + StringBuilder s = new StringBuilder(); + + for (int i = 0; i < list.Count; i++) + { + s.Append(list[i]); + if (i < list.Count - 1) + s.Append(','); + } + + _data[RemoveUnderscore(memberName)] = s.ToString(); + } + + /// + /// Set data for memberName to a compact representation of list. + /// If list is not strictly increasing or contains negative numbers, the results are undefined. + /// + public void SetListIntCompact(string memberName, List list) + { + StringBuilder s = new StringBuilder(); + int startRun, i; + + i = 0; + while (i < list.Count) + { + if (list[i] < 0) // This causes parsing problems for ActionReadOnlyAttributes + throw new ArgumentException("negative element in list"); + //if (i > 0 && list[i] <= list[i - 1]) + //throw new ArgumentException("list must be strictly increasing"); + + if (s.Length > 0) + s.Append(','); + s.Append(list[i]); + startRun = i; + while (i < list.Count-1 && list[i+1] == list[i] + 1) + { + i++; + } + if (i > startRun+1) + { + s.Append('-'); + s.Append(list[i]); + i++; + } + else if (i == startRun) + i++; + } + + _data[RemoveUnderscore(memberName)] = s.ToString(); + } + + public IReadOnlyDictionary Data => _data; + + private static string RemoveUnderscore(string memberName) + { + return memberName.Substring(1); + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Base/ActionReadOnlyAttributes.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Base/ActionReadOnlyAttributes.cs new file mode 100644 index 00000000..567a5978 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Base/ActionReadOnlyAttributes.cs @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using System.Collections.Generic; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Actions.Base +{ + public class ActionReadOnlyAttributes + { + private readonly IDsmModel _model; + private readonly IReadOnlyDictionary _data; + + public ActionReadOnlyAttributes(IDsmModel model, IReadOnlyDictionary data) + { + _data = data; + _model = model; + } + + public string GetString(string memberName) + { + return _data[RemoveUnderscore(memberName)]; + } + + public int GetInt(string memberName) + { + return int.Parse(_data[RemoveUnderscore(memberName)]); + } + + public int? GetNullableInt(string memberName) + { + int? value = null; + + int number; + if (_data.ContainsKey(RemoveUnderscore(memberName)) && + int.TryParse(_data[RemoveUnderscore(memberName)], out number)) + { + value = number; + } + + return value; + } + + public List GetListInt(string memberName) + { + List list = new List(); + string s = _data.GetValueOrDefault(RemoveUnderscore(memberName)); + + foreach (string item in s.Split(',')) + { + int value; + if (int.TryParse(item, out value)) + { + list.Add(value); + } + } + + return list; + } + + + public List GetListIntCompact(string memberName) + { + List list = new List(); + string s = _data.GetValueOrDefault(RemoveUnderscore(memberName)); + + foreach (string item in s.Split(',')) + { + if (item.LastIndexOf('-') > 0) // A range (and not a negative number)? + { + int start, end; + string[] limits = item.Split('-'); + + if (limits.Length > 2 || + !int.TryParse(limits[0], out start) || !int.TryParse(limits[1], out end)) + throw new ArgumentException("malformed data string"); + + for (int i = start; i <= end; i++) + list.Add(i); + } + else + { + int value; + if (int.TryParse(item, out value)) + { + list.Add(value); + } + } + } + + return list; + } + + + public IDsmElement GetElement(string memberName) + { + int id = GetInt(memberName); + return _model.GetElementById(id) ?? + _model.GetDeletedElementById(id); + } + + public IDsmRelation GetRelation(string memberName) + { + int id = GetInt(memberName); + return _model.GetRelationById(id) ?? + _model.GetDeletedRelationById(id); + } + + public IDsmElement GetRelationConsumer(string memberName) + { + IDsmElement consumer = null; + IDsmRelation relation = GetRelation(memberName); + if (relation != null) + { + consumer = _model.GetElementById(relation.Consumer.Id) ?? + _model.GetDeletedElementById(relation.Consumer.Id); + } + return consumer; + } + + public IDsmElement GetRelationProvider(string memberName) + { + IDsmElement provider = null; + IDsmRelation relation = GetRelation(memberName); + if (relation != null) + { + provider = _model.GetElementById(relation.Provider.Id) ?? + _model.GetDeletedElementById(relation.Provider.Id); + } + return provider; + } + + private static string RemoveUnderscore(string memberName) + { + return memberName.Substring(1); + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementChangeNameAction.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementChangeNameAction.cs new file mode 100644 index 00000000..a9016a68 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementChangeNameAction.cs @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Application.Actions.Base; +using DsmSuite.DsmViewer.Application.Actions.Management; +using DsmSuite.DsmViewer.Application.Interfaces; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Actions.Element +{ + public class ElementChangeNameAction : IAction + { + private readonly IDsmModel _model; + private readonly IActionContext _actionContext; + private readonly IDsmElement _element; + private readonly string _old; + private readonly string _new; + + public const ActionType RegisteredType = ActionType.ElementChangeName; + + public ElementChangeNameAction(IDsmModel model, IActionContext context, IReadOnlyDictionary data) + { + _model = model; + _actionContext = context; + if (_model != null && data != null) + { + ActionReadOnlyAttributes attributes = new ActionReadOnlyAttributes(_model, data); + + _element = attributes.GetElement(nameof(_element)); + _old = attributes.GetString(nameof(_old)); + _new = attributes.GetString(nameof(_new)); + } + } + + + public ElementChangeNameAction(IDsmModel model, IDsmElement element, string name) + { + _model = model; + _element = element; + _old = _element.Name; + _new = name; + } + + public ActionType Type => RegisteredType; + public string Title => "Change element name"; + public string Description => $"element={_element.Fullname} name={_old}->{_new}"; + + public object Do() + { + _model.ChangeElementName(_element, _new); + return null; + } + + public void Undo() + { + _model.ChangeElementName(_element, _old); + } + + public bool IsValid() + { + return (_model != null) && + (_element != null) && + (_old != null) && + (_new != null); + } + + public IReadOnlyDictionary Data + { + get + { + ActionAttributes attributes = new ActionAttributes(); + attributes.SetInt(nameof(_element), _element.Id); + attributes.SetString(nameof(_old), _old); + attributes.SetString(nameof(_new), _new); + return attributes.Data; + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementChangeParentAction.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementChangeParentAction.cs new file mode 100644 index 00000000..73987681 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementChangeParentAction.cs @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Application.Actions.Base; +using DsmSuite.DsmViewer.Application.Actions.Management; +using DsmSuite.DsmViewer.Application.Interfaces; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Actions.Element +{ + public class ElementChangeParentAction : IAction + { + private readonly IDsmModel _model; + private readonly IActionContext _actionContext; + private readonly IDsmElement _element; + private readonly IDsmElement _oldParent; + private readonly int _oldIndex; + private readonly string _oldName; + private readonly IDsmElement _newParent; + private readonly int _newIndex; + private string _newName; + + public const ActionType RegisteredType = ActionType.ElementChangeParent; + + public ElementChangeParentAction(IDsmModel model, IActionContext context, IReadOnlyDictionary data) + { + _model = model; + _actionContext = context; + if (_model != null && data != null) + { + ActionReadOnlyAttributes attributes = new ActionReadOnlyAttributes(_model, data); + + _element = attributes.GetElement(nameof(_element)); + _oldParent = attributes.GetElement(nameof(_oldParent)); + _oldIndex = attributes.GetInt(nameof(_oldIndex)); + _oldName = attributes.GetString(nameof(_oldName)); + _newParent = attributes.GetElement(nameof(_newParent)); + _newIndex = attributes.GetInt(nameof(_newIndex)); + _newName = attributes.GetString(nameof(_newName)); + } + } + + public ElementChangeParentAction(IDsmModel model, IDsmElement element, IDsmElement newParent, int index) + { + _model = model; + _element = element; + + _oldParent = element.Parent; + _oldIndex = _oldParent.IndexOfChild(element); + _oldName = element.Name; + + _newParent = newParent; + _newIndex = index; + _newName = element.Name; + } + + public ActionType Type => RegisteredType; + public string Title => "Change element parent"; + public string Description => $"element={_element.Fullname} parent={_oldParent.Fullname}->{_newParent.Fullname}"; + + public object Do() + { + // Rename to avoid duplicate names + if (_newParent.ContainsChildWithName(_oldName)) + { + _newName += " (duplicate)"; + _model.ChangeElementName(_element, _newName); + } + + _model.ChangeElementParent(_element, _newParent, _newIndex); + _model.AssignElementOrder(); + return null; + } + + public void Undo() + { + _model.ChangeElementParent(_element, _oldParent, _oldIndex); + _model.AssignElementOrder(); + + // Restore original name + if (_oldName != _newName) + { + _model.ChangeElementName(_element, _oldName); + } + } + + public bool IsValid() + { + return (_model != null) && + (_element != null) && + (_oldParent != null) && + (_oldName != null) && + (_newParent != null) && + (_newName != null); + } + + public IReadOnlyDictionary Data + { + get + { + ActionAttributes attributes = new ActionAttributes(); + attributes.SetInt(nameof(_element), _element.Id); + attributes.SetInt(nameof(_oldParent), _oldParent.Id); + attributes.SetString(nameof(_oldName), _oldName); + attributes.SetInt(nameof(_oldIndex), _oldIndex); + attributes.SetInt(nameof(_newParent), _newParent.Id); + attributes.SetString(nameof(_newName), _newName); + attributes.SetInt(nameof(_newIndex), _newIndex); + return attributes.Data; + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementChangeTypeAction.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementChangeTypeAction.cs new file mode 100644 index 00000000..e1c0aeff --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementChangeTypeAction.cs @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Application.Actions.Base; +using DsmSuite.DsmViewer.Application.Actions.Management; +using DsmSuite.DsmViewer.Application.Interfaces; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Actions.Element +{ + public class ElementChangeTypeAction : IAction + { + private readonly IDsmModel _model; + private readonly IActionContext _actionContext; + private readonly IDsmElement _element; + private readonly string _old; + private readonly string _new; + + public const ActionType RegisteredType = ActionType.ElementChangeType; + + public ElementChangeTypeAction(IDsmModel model, IActionContext context, IReadOnlyDictionary data) + { + _model = model; + _actionContext = context; + if (_model != null && data != null) + { + ActionReadOnlyAttributes attributes = new ActionReadOnlyAttributes(_model, data); + + _element = attributes.GetElement(nameof(_element)); + _old = attributes.GetString(nameof(_old)); + _new = attributes.GetString(nameof(_new)); + } + } + + public ElementChangeTypeAction(IDsmModel model, IDsmElement element, string type) + { + _model = model; + _element = element; + _old = _element.Type; + _new = type; + } + + public ActionType Type => RegisteredType; + public string Title => "Change element type"; + public string Description => $"element={_element.Fullname} type={_old}->{_new}"; + + public object Do() + { + _model.ChangeElementType(_element, _new); + return null; + } + + public void Undo() + { + _model.ChangeElementType(_element, _old); + } + + public bool IsValid() + { + return (_model != null) && + (_element != null) && + (_old != null) && + (_new != null); + } + + public IReadOnlyDictionary Data + { + get + { + ActionAttributes attributes = new ActionAttributes(); + attributes.SetInt(nameof(_element), _element.Id); + attributes.SetString(nameof(_old), _old); + attributes.SetString(nameof(_new), _new); + return attributes.Data; + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementCopyAction.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementCopyAction.cs new file mode 100644 index 00000000..aa037292 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementCopyAction.cs @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Application.Actions.Base; +using DsmSuite.DsmViewer.Application.Actions.Management; +using DsmSuite.DsmViewer.Application.Interfaces; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Actions.Element +{ + public class ElementCopyAction : IAction + { + private readonly IDsmModel _model; + private readonly IActionContext _actionContext; + private readonly IDsmElement _element; + private IDsmElement _elementCopy; + + public const ActionType RegisteredType = ActionType.ElementCopy; + + public ElementCopyAction(IDsmModel model, IDsmElement element, IActionContext actionContext) + { + _model = model; + _element = element; + _actionContext = actionContext; + _elementCopy = null; + } + + public ActionType Type => RegisteredType; + public string Title => "Copy element"; + public string Description => $"element={_element.Fullname}"; + + public object Do() + { + _elementCopy = _model.AddElement(_element.Name, _element.Type, null, null, null); + _actionContext.AddElementToClipboard(_elementCopy); + return null; + } + + public void Undo() + { + _actionContext.RemoveElementFromClipboard(_elementCopy); + } + + public bool IsValid() + { + return (_model != null) && + (_element != null); + } + + public IReadOnlyDictionary Data + { + get + { + ActionAttributes attributes = new ActionAttributes(); + attributes.SetInt(nameof(_element), _element.Id); + attributes.SetInt(nameof(_elementCopy), _elementCopy.Id); + return attributes.Data; + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementCreateAction.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementCreateAction.cs new file mode 100644 index 00000000..c22e40bb --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementCreateAction.cs @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Application.Actions.Base; +using DsmSuite.DsmViewer.Application.Actions.Management; +using DsmSuite.DsmViewer.Application.Interfaces; +using DsmSuite.DsmViewer.Model.Interfaces; +using System.Diagnostics; + +namespace DsmSuite.DsmViewer.Application.Actions.Element +{ + public class ElementCreateAction : IAction + { + private readonly IDsmModel _model; + private readonly IActionContext _actionContext; + private IDsmElement _element; + private readonly string _name; + private readonly string _type; + private readonly IDsmElement _parent; + private readonly int _index; + + public const ActionType RegisteredType = ActionType.ElementCreate; + + public ElementCreateAction(IDsmModel model, IActionContext context, IReadOnlyDictionary data) + { + _model = model; + _actionContext = context; + if (_model != null && data != null) + { + ActionReadOnlyAttributes attributes = new ActionReadOnlyAttributes(_model, data); + + _element = attributes.GetElement(nameof(_element)); + _name = attributes.GetString(nameof(_name)); + _type = attributes.GetString(nameof(_type)); + + int? parentId = attributes.GetNullableInt(nameof(_parent)); + if (parentId.HasValue) + { + _parent = _model.GetElementById(parentId.Value); + } + _index = attributes.GetInt(nameof(_index)); + } + } + + public ElementCreateAction(IDsmModel model, string name, string type, IDsmElement parent, int index) + { + _model = model; + _name = name; + _type = type; + _parent = parent; + _index = index; + } + + public ActionType Type => RegisteredType; + public string Title => "Create element"; + public string Description => $"name={_name} type={_type} parent={_parent.Fullname}"; + + public object Do() + { + _element = _model.AddElement(_name, _type, _parent.Id, _index, null); + Debug.Assert(_element != null); + + _model.AssignElementOrder(); + + return _element; + } + + public void Undo() + { + _model.RemoveElement(_element.Id); + _model.AssignElementOrder(); + } + + public bool IsValid() + { + return (_model != null) && + (_element != null) && + (_type != null) && + (_parent != null); + } + + public IReadOnlyDictionary Data + { + get + { + ActionAttributes attributes = new ActionAttributes(); + attributes.SetInt(nameof(_element), _element.Id); + attributes.SetString(nameof(_name), _name); + attributes.SetString(nameof(_type), _type); + attributes.SetNullableInt(nameof(_parent), _parent.Id); + attributes.SetInt(nameof(_index), _index); + return attributes.Data; + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementCutAction.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementCutAction.cs new file mode 100644 index 00000000..b5907d42 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementCutAction.cs @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Application.Actions.Base; +using DsmSuite.DsmViewer.Application.Actions.Management; +using DsmSuite.DsmViewer.Application.Interfaces; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Actions.Element +{ + public class ElementCutAction : IAction + { + private readonly IDsmModel _model; + private readonly IActionContext _actionContext; + private readonly IDsmElement _element; + + public const ActionType RegisteredType = ActionType.ElementCut; + + public ElementCutAction(IDsmModel model, IDsmElement element, IActionContext actionContext) + { + _model = model; + _element = element; + _actionContext = actionContext; + } + + public ActionType Type => RegisteredType; + public string Title => "Cut element"; + public string Description => $"element={_element.Fullname}"; + + public object Do() + { + _actionContext.AddElementToClipboard(_element); + _model.RemoveElement(_element.Id); + _model.AssignElementOrder(); + return null; + } + + public void Undo() + { + _actionContext.RemoveElementFromClipboard(_element); + _model.UnremoveElement(_element.Id); + _model.AssignElementOrder(); + } + + public bool IsValid() + { + return (_model != null) && + (_element != null); + } + + public IReadOnlyDictionary Data + { + get + { + ActionAttributes attributes = new ActionAttributes(); + attributes.SetInt(nameof(_element), _element.Id); + return attributes.Data; + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementDeleteAction.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementDeleteAction.cs new file mode 100644 index 00000000..c97f3881 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementDeleteAction.cs @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Application.Actions.Base; +using DsmSuite.DsmViewer.Application.Actions.Management; +using DsmSuite.DsmViewer.Application.Interfaces; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Actions.Element +{ + public class ElementDeleteAction : IAction + { + private readonly IDsmModel _model; + private readonly IActionContext _actionContext; + private readonly IDsmElement _element; + + public const ActionType RegisteredType = ActionType.ElementDelete; + + public ElementDeleteAction(IDsmModel model, IActionContext context, IReadOnlyDictionary data) + { + _model = model; + _actionContext = context; + if (_model != null && data != null) + { + ActionReadOnlyAttributes attributes = new ActionReadOnlyAttributes(_model, data); + + _element = attributes.GetElement(nameof(_element)); + } + } + + public ElementDeleteAction(IDsmModel model, IDsmElement element) + { + _model = model; + _element = element; + } + + public ActionType Type => RegisteredType; + public string Title => "Delete element"; + public string Description => $"element={_element.Fullname}"; + + public object Do() + { + _model.RemoveElement(_element.Id); + _model.AssignElementOrder(); + return null; + } + + public void Undo() + { + _model.UnremoveElement(_element.Id); + _model.AssignElementOrder(); + } + + public bool IsValid() + { + return (_model != null) && + (_element != null); + } + + public IReadOnlyDictionary Data + { + get + { + ActionAttributes attributes = new ActionAttributes(); + attributes.SetInt(nameof(_element), _element.Id); + return attributes.Data; + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementMoveDownAction.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementMoveDownAction.cs new file mode 100644 index 00000000..08fc476b --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementMoveDownAction.cs @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Application.Actions.Base; +using DsmSuite.DsmViewer.Application.Actions.Management; +using DsmSuite.DsmViewer.Application.Interfaces; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Actions.Element +{ + public class ElementMoveDownAction : IAction + { + private readonly IDsmModel _model; + private readonly IActionContext _actionContext; + private readonly IDsmElement _element; + + public const ActionType RegisteredType = ActionType.ElementMoveDown; + + public ElementMoveDownAction(IDsmModel model, IActionContext context, IReadOnlyDictionary data) + { + _model = model; + _actionContext = context; + if (_model != null && data != null) + _element = new ActionReadOnlyAttributes(_model, data).GetElement(nameof(_element)); + } + + public ElementMoveDownAction(IDsmModel model, IDsmElement element) + { + _model = model; + _element = element; + } + + public ActionType Type => RegisteredType; + public string Title => "Move down element"; + public string Description => $"element={_element.Fullname}"; + + public object Do() + { + IDsmElement nextElement = _model.NextSibling(_element); + if (nextElement != null) + { + _model.Swap(_element, nextElement); + _model.AssignElementOrder(); + } + + return null; + } + + public void Undo() + { + IDsmElement previousElement = _model.PreviousSibling(_element); + if (previousElement != null) + { + _model.Swap(previousElement, _element); + _model.AssignElementOrder(); + } + } + + public bool IsValid() + { + return (_model != null) && + (_element != null); + } + + public IReadOnlyDictionary Data + { + get + { + ActionAttributes attributes = new ActionAttributes(); + attributes.SetInt(nameof(_element), _element.Id); + return attributes.Data; + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementMoveUpAction.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementMoveUpAction.cs new file mode 100644 index 00000000..2eb769f4 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementMoveUpAction.cs @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Application.Actions.Base; +using DsmSuite.DsmViewer.Application.Actions.Management; +using DsmSuite.DsmViewer.Application.Interfaces; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Actions.Element +{ + public class ElementMoveUpAction : IAction + { + private readonly IDsmModel _model; + private readonly IActionContext _actionContext; + private readonly IDsmElement _element; + + public const ActionType RegisteredType = ActionType.ElementMoveUp; + + public ElementMoveUpAction(IDsmModel model, IActionContext context, IReadOnlyDictionary data) + { + _model = model; + _actionContext = context; + if (_model != null && data != null) + _element = new ActionReadOnlyAttributes(_model, data).GetElement(nameof(_element)); + } + + public ElementMoveUpAction(IDsmModel model, IDsmElement element) + { + _model = model; + _element = element; + } + + public ActionType Type => RegisteredType; + public string Title => "Move up element"; + public string Description => $"element={_element.Fullname}"; + + public object Do() + { + IDsmElement previousElement = _model.PreviousSibling(_element); + if (previousElement != null) + { + _model.Swap(_element, previousElement); + _model.AssignElementOrder(); + } + + return null; + } + + public void Undo() + { + IDsmElement nextElement = _model.NextSibling(_element); + if (nextElement != null) + { + _model.Swap(nextElement, _element); + _model.AssignElementOrder(); + } + } + + public bool IsValid() + { + return (_model != null) && + (_element != null); + } + + public IReadOnlyDictionary Data + { + get + { + ActionAttributes attributes = new ActionAttributes(); + attributes.SetInt(nameof(_element), _element.Id); + return attributes.Data; + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementPasteAction.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementPasteAction.cs new file mode 100644 index 00000000..d709b9f3 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementPasteAction.cs @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Application.Actions.Base; +using DsmSuite.DsmViewer.Application.Actions.Management; +using DsmSuite.DsmViewer.Application.Interfaces; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Actions.Element +{ + public class ElementPasteAction : IAction + { + private readonly IDsmModel _model; + private readonly IActionContext _actionContext; + private readonly IDsmElement _element; + private readonly IDsmElement _oldParent; + private readonly int _oldIndex; + private readonly string _oldName; + private readonly IDsmElement _newParent; + private readonly int _newIndex; + private string _newName; + + public const ActionType RegisteredType = ActionType.ElementPaste; + + public ElementPasteAction(IDsmModel model, IDsmElement newParent, int index, IActionContext actionContext) + { + _model = model; + + _actionContext = actionContext; + _element = _actionContext.GetElementOnClipboard(); + + _oldParent = _element.Parent; + _oldIndex = _oldParent.IndexOfChild(_element); + _oldName = _element.Name; + + _newParent = newParent; + _newIndex = index; + _newName = _element.Name; + } + + public ActionType Type => RegisteredType; + public string Title => "Paste element"; + public string Description => $"element={_actionContext.GetElementOnClipboard().Fullname}"; + + public object Do() + { + // Rename to avoid duplicate names + if (_newParent.ContainsChildWithName(_oldName)) + { + _newName += " (duplicate)"; + _model.ChangeElementName(_element, _newName); + } + + _model.ChangeElementParent(_element, _newParent, _newIndex); + _model.AssignElementOrder(); + + _actionContext.RemoveElementFromClipboard(_element); + return null; + } + + public void Undo() + { + _model.ChangeElementParent(_element, _oldParent, _oldIndex); + _model.AssignElementOrder(); + + // Restore original name + if (_oldName != _newName) + { + _model.ChangeElementName(_element, _oldName); + } + } + + public bool IsValid() + { + return (_model != null) && + (_element != null) && + (_actionContext != null) && + (_actionContext.IsElementOnClipboard()) && + (_oldParent != null) && + (_oldName != null) && + (_newParent != null) && + (_newName != null); + } + + public IReadOnlyDictionary Data + { + get + { + ActionAttributes attributes = new ActionAttributes(); + attributes.SetInt(nameof(_element), _element.Id); + return attributes.Data; + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementSortAction.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementSortAction.cs new file mode 100644 index 00000000..ad48001c --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementSortAction.cs @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Application.Actions.Base; +using DsmSuite.DsmViewer.Application.Actions.Management; +using DsmSuite.DsmViewer.Application.Interfaces; +using DsmSuite.DsmViewer.Application.Sorting; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Actions.Element +{ + public class ElementSortAction : IAction + { + private readonly IDsmModel _model; + private readonly IActionContext _actionContext; + private readonly IDsmElement _element; + private readonly string _algorithm; + private List _order; + + public const ActionType RegisteredType = ActionType.ElementSort; + + public ElementSortAction(IDsmModel model, IActionContext context, IReadOnlyDictionary data) + { + _model = model; + _actionContext = context; + if (_model != null && data != null) + { + ActionReadOnlyAttributes attributes = new ActionReadOnlyAttributes(_model, data); + + _element = attributes.GetElement(nameof(_element)); + _algorithm = attributes.GetString(nameof(_algorithm)); + _order = attributes.GetListInt(nameof(_order)); + } + } + + public ElementSortAction(IDsmModel model, IDsmElement element, string algorithm) + { + _model = model; + _element = element; + _algorithm = algorithm; + _order = null; + } + + public ActionType Type => RegisteredType; + public string Title => "Partition element"; + public string Description => $"element={_element.Fullname} algorithm={_algorithm}"; + + public object Do() + { + ISortAlgorithm sortAlgorithm = SortAlgorithmFactory.CreateAlgorithm(_model, _element, _algorithm); + SortResult sortResult = sortAlgorithm.Sort(); + _model.ReorderChildren(_element, sortResult); + _order = sortResult.GetOrder(); + + _model.AssignElementOrder(); + + return null; + } + + public void Undo() + { + SortResult sortResult = new SortResult(_order); + sortResult.InvertOrder(); + _model.ReorderChildren(_element, sortResult); + + _model.AssignElementOrder(); + } + + public bool IsValid() + { + return (_model != null) && + (_element != null) && + (_algorithm != null) && + (_order != null); + } + + public IReadOnlyDictionary Data + { + get + { + ActionAttributes attributes = new ActionAttributes(); + attributes.SetInt(nameof(_element), _element.Id); + attributes.SetString(nameof(_algorithm), _algorithm); + attributes.SetListInt(nameof(_order), _order); + return attributes.Data; + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementSortRecursiveAction.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementSortRecursiveAction.cs new file mode 100644 index 00000000..40b9810a --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Element/ElementSortRecursiveAction.cs @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.Common.Util; +using DsmSuite.DsmViewer.Application.Actions.Base; +using DsmSuite.DsmViewer.Application.Actions.Management; +using DsmSuite.DsmViewer.Application.Interfaces; +using DsmSuite.DsmViewer.Application.Sorting; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Actions.Element +{ + /// + /// Recursively sort the subtree rooted at an element. Only sorts elements that are in the tree. + /// + public class ElementSortRecursiveAction : IMultiAction + { + private readonly IDsmModel _model; + private readonly IActionContext _actionContext; + private readonly IDsmElement _element; + private readonly string _algorithm; + private List _actions; // The sort actions on descendant elements (possibly empty) + + public const ActionType RegisteredType = ActionType.ElementSortRecursive; + + public ElementSortRecursiveAction(IDsmModel model, IActionContext context, + IReadOnlyDictionary data, IEnumerable actions) + { + _model = model; + _actionContext = context; + if (_model != null && data != null) + { + ActionReadOnlyAttributes attributes = new ActionReadOnlyAttributes(_model, data); + + _element = attributes.GetElement(nameof(_element)); + _algorithm = attributes.GetString(nameof(_algorithm)); + _actions = new List(actions); + } + } + + public ElementSortRecursiveAction(IDsmModel model, IDsmElement element, string algorithm) + { + _model = model; + _element = element; + _algorithm = algorithm; + _actions = new(); + } + + public ActionType Type => RegisteredType; + public string Title => "Partition element recursively"; + public string Description => $"element={_element.Fullname} algorithm={_algorithm}"; + + private void doRecursive(IDsmElement element) + { + if (!element.HasChildren) + return; + + Logger.LogInfo(element.Fullname); + + ElementSortAction action = new(_model, element, _algorithm); + action.Do(); + _actions.Add(action); + + foreach (IDsmElement child in element.AllChildren) + doRecursive(child); + } + + public object Do() + { + _actions.Clear(); + doRecursive(_element); + return null; + } + + public void Undo() + { + foreach (IAction action in _actions) + action.Undo(); + } + + public bool IsValid() + { + return _model != null && _element != null && _algorithm != null && _actions != null; + } + + public IReadOnlyDictionary Data + { + get + { + ActionAttributes attributes = new ActionAttributes(); + attributes.SetInt(nameof(_element), _element.Id); + attributes.SetString(nameof(_algorithm), _algorithm); + return attributes.Data; + } + } + + public IEnumerable Actions { get { return _actions; } } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Filtering/ShowElementContextAction.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Filtering/ShowElementContextAction.cs new file mode 100644 index 00000000..ae034c95 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Filtering/ShowElementContextAction.cs @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Application.Actions.Base; +using DsmSuite.DsmViewer.Application.Actions.Management; +using DsmSuite.DsmViewer.Application.Interfaces; +using DsmSuite.DsmViewer.Application.Queries; +using DsmSuite.DsmViewer.Model.Interfaces; +using System.Collections.Generic; +using System.Linq; +using static System.Net.Mime.MediaTypeNames; + +namespace DsmSuite.DsmViewer.Application.Actions.Filtering +{ + public class ShowElementContextAction : IAction + { + private readonly IDsmModel _model; + private readonly IDsmElement _provider; + private readonly IActionContext _actionContext; + private List _before; + + public const ActionType RegisteredType = ActionType.ShowElementContext; + + public ShowElementContextAction(IDsmModel model, IActionContext context, IReadOnlyDictionary data) + { + _model = model; + _actionContext = context; + + if (_model != null && data != null) + { + ActionReadOnlyAttributes atts = new(_model, data); + + _provider = atts.GetElement(nameof(_provider)); + _before = atts.GetListIntCompact(nameof(_before)); + } + } + + public ShowElementContextAction(IDsmModel model, IDsmElement provider) + { + _model = model; + _provider = provider; + _before = null; + } + + public ActionType Type => RegisteredType; + public string Title => "ShowElementContext"; + public string Description => $"provider={_provider?.Fullname}"; + + public object Do() + { + DsmQueries queries = new DsmQueries(_model); + + _before = new(); + foreach (IDsmElement e in _model.GetElements()) + if (e.IsIncludedInTree) + _before.Add(e.Id); + _before.Sort(); + + _model.IncludeInTree(_model.RootElement, false); + _model.IncludeInTree(_provider, true); + foreach (WeightedElement consumer in queries.FindConsumersOf(_provider)) + _model.IncludeInTree(consumer.Element, true); + foreach (WeightedElement provider in queries.FindProvidersFor(_provider)) + _model.IncludeInTree(provider.Element, true); + + return null; + } + + public void Undo() + { + _model.IncludeInTree(_model.RootElement, false); + foreach (int i in _before) + _model.GetElementById(i).IsIncludedInTree = true; + } + + public bool IsValid() + { + return _model != null && _provider != null; + } + + public IReadOnlyDictionary Data + { + get + { + ActionAttributes attributes = new(); + attributes.SetInt(nameof(_provider), _provider.Id); + attributes.SetListIntCompact(nameof(_before), _before); + return attributes.Data; + } + } + } +} + diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Filtering/ShowElementDetailAction.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Filtering/ShowElementDetailAction.cs new file mode 100644 index 00000000..2a1fbc6b --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Filtering/ShowElementDetailAction.cs @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Application.Actions.Base; +using DsmSuite.DsmViewer.Application.Actions.Management; +using DsmSuite.DsmViewer.Application.Interfaces; +using DsmSuite.DsmViewer.Model.Interfaces; +using System.Collections.Generic; +using System.Linq; + +namespace DsmSuite.DsmViewer.Application.Actions.Filtering +{ + public class ShowElementDetailAction : IAction + { + private readonly IDsmModel _model; + private readonly IDsmElement _provider, _consumer; + private readonly IActionContext _actionContext; + private List _before; + + public const ActionType RegisteredType = ActionType.ShowElementDetail; + + public ShowElementDetailAction(IDsmModel model, IActionContext context, IReadOnlyDictionary data) + { + _model = model; + _actionContext = context; + + if (_model != null && data != null) + { + int? id; + ActionReadOnlyAttributes atts = new(_model, data); + + id = atts.GetNullableInt(nameof(_provider)); + if (id.HasValue) + _provider = _model.GetElementById(id.Value); + id = atts.GetNullableInt(nameof(_consumer)); + if (id.HasValue) + _consumer = _model.GetElementById(id.Value); + _before = atts.GetListIntCompact(nameof(_before)); + } + } + + public ShowElementDetailAction(IDsmModel model, IDsmElement provider, IDsmElement consumer) + { + _model = model; + _provider = provider; + _consumer = consumer; + _before = null; + } + + public ActionType Type => RegisteredType; + public string Title => "ShowElementDetail"; + public string Description => $"provider={_provider?.Fullname} consumer={_consumer?.Fullname}"; + + public object Do() + { + _before = new(); + foreach (IDsmElement e in _model.GetElements()) + if (e.IsIncludedInTree) + _before.Add(e.Id); + _before.Sort(); + + _model.IncludeInTree(_model.RootElement, false); + if (_provider != null) + _model.IncludeInTree(_provider, true); + if (_consumer != null) + _model.IncludeInTree(_consumer, true); + return null; + } + + public void Undo() + { + _model.IncludeInTree(_model.RootElement, false); + foreach (int i in _before) + _model.GetElementById(i).IsIncludedInTree = true; + } + + public bool IsValid() + { + return _model != null && (_provider != null || _consumer != null); + } + + public IReadOnlyDictionary Data + { + get + { + ActionAttributes attributes = new(); + if (_provider != null) + attributes.SetInt(nameof(_provider), _provider.Id); + if (_consumer != null) + attributes.SetInt(nameof(_consumer), _consumer.Id); + attributes.SetListIntCompact(nameof(_before), _before); + return attributes.Data; + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Management/ActionContext.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Management/ActionContext.cs new file mode 100644 index 00000000..4b58b46b --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Management/ActionContext.cs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Actions.Management +{ + public class ActionContext : IActionContext + { + private IDsmElement _element; + + public void AddElementToClipboard(IDsmElement element) + { + _element = element; + } + + public void RemoveElementFromClipboard(IDsmElement element) + { + _element = null; + } + + public IDsmElement GetElementOnClipboard() + { + return _element; + } + + public bool IsElementOnClipboard() + { + return (_element == null); + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Management/ActionManager.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Management/ActionManager.cs new file mode 100644 index 00000000..c7f38078 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Management/ActionManager.cs @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.Common.Util; +using DsmSuite.DsmViewer.Application.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Actions.Management +{ + /// + /// Manages user actions. + /// This include executing them, undo/redo and keeping a list of all executed actions. + /// + public class ActionManager : IActionManager + { + private readonly Stack _undoActionStack; + private readonly Stack _redoActionStack; + private readonly ActionContext _actionContext; + + public event EventHandler ActionPerformed; + + public ActionManager() + { + _undoActionStack = new Stack(); + _redoActionStack = new Stack(); + _actionContext = new ActionContext(); + } + + public bool Validate() + { + bool valid = true; + + foreach (var action in _undoActionStack) + { + if (!action.IsValid()) + { + Logger.LogError($"Invalid action: {action.ToString()}"); + valid = false; + } + } + + foreach (var action in _redoActionStack) + { + if (!action.IsValid()) + { + valid = false; + } + } + + return valid; + } + + public object Execute(IAction action) + { + _undoActionStack.Push(action); + _redoActionStack.Clear(); + object result = action.Do(); + ActionPerformed?.Invoke(this, EventArgs.Empty); + return result; + } + + public void Add(IAction action) + { + _undoActionStack.Push(action); + } + + public bool CanUndo() + { + return _undoActionStack.Count > 0; + } + + public IAction GetCurrentUndoAction() + { + IAction action = null; + if (_undoActionStack.Count > 0) + { + action = _undoActionStack.Peek(); + } + return action; + } + + public void Undo() + { + if (_undoActionStack.Count > 0) + { + IAction action = _undoActionStack.Pop(); + if (action != null) + { + _redoActionStack.Push(action); + action.Undo(); + ActionPerformed?.Invoke(this, EventArgs.Empty); + Logger.LogInfo($"Undo :{action.Description}"); + } + } + } + + public bool CanRedo() + { + return _redoActionStack.Count > 0; + } + + public IAction GetCurrentRedoAction() + { + IAction action = null; + if (_redoActionStack.Count > 0) + { + action = _redoActionStack.Peek(); + } + return action; + } + + public void Redo() + { + if (_redoActionStack.Count > 0) + { + IAction action = _redoActionStack.Pop(); + if (action != null) + { + _undoActionStack.Push(action); + action.Do(); + ActionPerformed?.Invoke(this, EventArgs.Empty); + Logger.LogInfo($"Redo :{action.Description}"); + } + } + } + + /// + /// Undo/redo actions until action is at the top of the undo stack. + /// + /// + public void Goto(IAction action) + { + if (_undoActionStack.Contains(action)) + { + while (_undoActionStack.Peek() != action) + { + Undo(); + } + } + else if (_redoActionStack.Contains(action)) + { + do + { + Redo(); + } + while (_undoActionStack.Peek() != action); + } + else + Logger.LogError($"Action {action} not in ActionManager"); + } + + public void Clear() + { + _undoActionStack.Clear(); + _redoActionStack.Clear(); + } + + public IEnumerable GetActionsInReverseChronologicalOrder() + { + return _undoActionStack; + } + + public IEnumerable GetActionsInChronologicalOrder() + { + return _undoActionStack.Reverse(); + } + + /// + /// Return the actions for redo in the order in which they were originally executed. + /// This is also the order in which they will be redone. + /// + public IEnumerable GetRedoActionsInChronologicalOrder() + { + return _redoActionStack; + } + + public IActionContext GetContext() + { + return _actionContext; + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Management/ActionStore.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Management/ActionStore.cs new file mode 100644 index 00000000..ef6007a0 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Management/ActionStore.cs @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using System; +using System.Collections.Generic; +using System.Reflection.Metadata.Ecma335; +using DsmSuite.Common.Util; +using DsmSuite.DsmViewer.Application.Actions.Element; +using DsmSuite.DsmViewer.Application.Actions.Filtering; +using DsmSuite.DsmViewer.Application.Actions.Relation; +using DsmSuite.DsmViewer.Application.Actions.Snapshot; +using DsmSuite.DsmViewer.Application.Interfaces; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Actions.Management +{ + /// + /// Stores/restores user actions to/from the model. + /// + /// + /// The ActionType enum tags are used as the action string for the model. + /// + public class ActionStore + { + // Translate ActionType's to the implementing classes. + // The ActionType enum tags are used as the action string for the model. + // TODO Isn't a string->Type dictionary more suitable? + // TODO Make certain (compile/runtime) all actions are indeed in the table (cut/copy/paste currently aren't) + // otherwise we can't load model that we saved. + private readonly Dictionary _types; + + public ActionStore() + { + _types = new Dictionary(); + RegisterActionTypes(); + } + + + /// + /// Loads all actions from the model into the ActionManager. + /// Unrecognized actions are logged but otherwise ignored. + /// If any action cannot be loaded correctly, the ActionManager is cleared. + /// + public void LoadFromModel(IActionManager actionManager, IDsmModel model) + { + actionManager.Clear(); + foreach (IDsmAction action in model.GetActions()) + { + IAction instance = LoadAction(actionManager, model, action); + if (instance != null) + actionManager.Add(instance); + else + Logger.LogError($"Cannot instantiate action {action.Id} {action.Type}."); + } + + if (!actionManager.Validate()) + { + Logger.LogWarning($"Invalid action found."); + actionManager.Clear(); + } + } + + + private IAction LoadAction(IActionManager actionManager, IDsmModel model, IDsmAction action) + { + ActionType actionType; + Type type; + object[] args; + + if (!ActionType.TryParse(action.Type, out actionType)) + Logger.LogWarning($"Unknown action {action.Type} in model."); + if (!_types.TryGetValue(actionType, out type)) + Logger.LogError($"Action {action.Type} not in table."); + + if (action.Actions == null) + args = [model, actionManager.GetContext(), action.Data]; + else + { + List subactions = new(); + foreach (IDsmAction subaction in action.Actions) + { + IAction a = LoadAction(actionManager, model, subaction); + if (a == null) + return null; + subactions.Add(a); + } + + args = [model, actionManager.GetContext(), action.Data, subactions]; + } + + return Activator.CreateInstance(type, args) as IAction; + } + + + /// + /// Saves all actions in the actionManager to the model. + /// + public void SaveToModel(IActionManager actionManager, IDsmModel model) + { + if (actionManager.Validate()) + { + model.ClearActions(); + foreach (IAction action in actionManager.GetActionsInChronologicalOrder()) + { + model.AddAction(action.Type.ToString(), action.Data, new MultiActionDTO(action).Actions); + } + } + } + + + // Strings here must be kept in sync with MultiAction. + private void RegisterActionTypes() + { + //TODO cut/copy/paste actions not present + _types[ElementChangeNameAction.RegisteredType] = typeof(ElementChangeNameAction); + _types[ElementChangeTypeAction.RegisteredType] = typeof(ElementChangeTypeAction); + _types[ElementChangeParentAction.RegisteredType] = typeof(ElementChangeParentAction); + _types[ElementCreateAction.RegisteredType] = typeof(ElementCreateAction); + _types[ElementDeleteAction.RegisteredType] = typeof(ElementDeleteAction); + _types[ElementMoveDownAction.RegisteredType] = typeof(ElementMoveDownAction); + _types[ElementMoveUpAction.RegisteredType] = typeof(ElementMoveUpAction); + _types[ElementSortAction.RegisteredType] = typeof(ElementSortAction); + _types[ElementSortRecursiveAction.RegisteredType] = typeof(ElementSortRecursiveAction); + + _types[ShowElementDetailAction.RegisteredType] = typeof(ShowElementDetailAction); + _types[ShowElementContextAction.RegisteredType] = typeof(ShowElementContextAction); + + _types[RelationChangeTypeAction.RegisteredType] = typeof(RelationChangeTypeAction); + _types[RelationChangeWeightAction.RegisteredType] = typeof(RelationChangeWeightAction); + _types[RelationCreateAction.RegisteredType] = typeof(RelationCreateAction); + _types[RelationDeleteAction.RegisteredType] = typeof(RelationDeleteAction); + + _types[MakeSnapshotAction.RegisteredType] = typeof(MakeSnapshotAction); + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Management/IActionContext.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Management/IActionContext.cs new file mode 100644 index 00000000..a68748ad --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Management/IActionContext.cs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Actions.Management +{ + public interface IActionContext + { + void AddElementToClipboard(IDsmElement element); + void RemoveElementFromClipboard(IDsmElement element); + IDsmElement GetElementOnClipboard(); + bool IsElementOnClipboard(); + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Management/IActionManager.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Management/IActionManager.cs new file mode 100644 index 00000000..1fa1fc72 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Management/IActionManager.cs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Application.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Actions.Management +{ + public interface IActionManager + { + bool Validate(); + void Clear(); + void Add(IAction action); + object Execute(IAction action); + IEnumerable GetActionsInChronologicalOrder(); + IActionContext GetContext(); + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Management/MultiActionDTO.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Management/MultiActionDTO.cs new file mode 100644 index 00000000..018262ad --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Management/MultiActionDTO.cs @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DsmSuite.DsmViewer.Application.Interfaces; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Actions.Management +{ + /// + /// A recursive representation of an IMultiAction, used by the ActionStore for saving to the DsmModel. + /// Id is always 0. + /// + public class MultiActionDTO : IDsmAction + { + public int Id { get; } + + public string Type { get; } + + public IReadOnlyDictionary Data { get; } + + public IEnumerable Actions { get; } + + public MultiActionDTO(IAction action) + { + Id = 0; + Type = action.Type.ToString(); // Must be kept in sync with the code in ActionStore + Data = action.Data; + Actions = (action as IMultiAction)?.Actions.Select(a => new MultiActionDTO(a)); + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Relation/RelationChangeTypeAction.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Relation/RelationChangeTypeAction.cs new file mode 100644 index 00000000..8aa852f4 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Relation/RelationChangeTypeAction.cs @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Application.Actions.Base; +using DsmSuite.DsmViewer.Application.Actions.Management; +using DsmSuite.DsmViewer.Application.Interfaces; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Actions.Relation +{ + public class RelationChangeTypeAction : IAction + { + private readonly IDsmModel _model; + private readonly IActionContext _actionContext; + private readonly IDsmRelation _relation; + private readonly IDsmElement _consumer; + private readonly IDsmElement _provider; + private readonly string _old; + private readonly string _new; + + public const ActionType RegisteredType = ActionType.RelationChangeType; + + public RelationChangeTypeAction(IDsmModel model, IActionContext context, IReadOnlyDictionary data) + { + _model = model; + _actionContext = context; + if (_model != null && data != null) + { + ActionReadOnlyAttributes attributes = new ActionReadOnlyAttributes(_model, data); + + _relation = attributes.GetRelation(nameof(_relation)); + _consumer = attributes.GetRelationConsumer(nameof(_relation)); + _provider = attributes.GetRelationProvider(nameof(_relation)); + _old = attributes.GetString(nameof(_old)); + _new = attributes.GetString(nameof(_new)); + } + } + + public RelationChangeTypeAction(IDsmModel model, IDsmRelation relation, string type) + { + _model = model; + _relation = relation; + _consumer = model.GetElementById(_relation.Consumer.Id); + _provider = model.GetElementById(_relation.Provider.Id); + _old = relation.Type; + _new = type; + } + + public ActionType Type => RegisteredType; + public string Title => "Change relation type"; + public string Description => $"consumer={_consumer.Fullname} provider={_provider.Fullname} type={_old}->{_new}"; + + public object Do() + { + _model.ChangeRelationType(_relation, _new); + return null; + } + + public void Undo() + { + _model.ChangeRelationType(_relation, _old); + } + + public bool IsValid() + { + return (_model != null) && (_relation != null) && (_old != null) && (_new != null); + } + + public IReadOnlyDictionary Data + { + get + { + ActionAttributes attributes = new ActionAttributes(); + attributes.SetInt(nameof(_relation), _relation.Id); + attributes.SetString(nameof(_new), _new); + attributes.SetString(nameof(_old), _old); + return attributes.Data; + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Relation/RelationChangeWeightAction.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Relation/RelationChangeWeightAction.cs new file mode 100644 index 00000000..861b0ca4 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Relation/RelationChangeWeightAction.cs @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Application.Actions.Base; +using DsmSuite.DsmViewer.Application.Actions.Management; +using DsmSuite.DsmViewer.Application.Interfaces; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Actions.Relation +{ + public class RelationChangeWeightAction : IAction + { + private readonly IDsmModel _model; + private readonly IActionContext _actionContext; + private readonly IDsmRelation _relation; + private readonly IDsmElement _consumer; + private readonly IDsmElement _provider; + private readonly int _old; + private readonly int _new; + + public const ActionType RegisteredType = ActionType.RelationChangeWeight; + + public RelationChangeWeightAction(IDsmModel model, IActionContext context, IReadOnlyDictionary data) + { + _model = model; + _actionContext = context; + if (_model != null && data != null) + { + ActionReadOnlyAttributes attributes = new ActionReadOnlyAttributes(_model, data); + + _relation = attributes.GetRelation(nameof(_relation)); + _consumer = attributes.GetRelationConsumer(nameof(_relation)); + _provider = attributes.GetRelationProvider(nameof(_relation)); + _old = attributes.GetInt(nameof(_old)); + _new = attributes.GetInt(nameof(_new)); + } + } + + public RelationChangeWeightAction(IDsmModel model, IDsmRelation relation, int weight) + { + _model = model; + _relation = relation; + _consumer = model.GetElementById(_relation.Consumer.Id); + _provider = model.GetElementById(_relation.Provider.Id); + _old = relation.Weight; + _new = weight; + } + + public ActionType Type => RegisteredType; + public string Title => "Change relation weight"; + public string Description => $"consumer={_consumer.Fullname} provider={_provider.Fullname} type={_relation.Type} weight={_old}->{_new}"; + + public object Do() + { + _model.ChangeRelationWeight(_relation, _new); + return null; + } + + public void Undo() + { + _model.ChangeRelationWeight(_relation, _old); + } + + public bool IsValid() + { + return (_model != null) && + (_relation != null); + } + + public IReadOnlyDictionary Data + { + get + { + ActionAttributes attributes = new ActionAttributes(); + attributes.SetInt(nameof(_relation), _relation.Id); + attributes.SetInt(nameof(_old), _old); + attributes.SetInt(nameof(_new), _new); + return attributes.Data; + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Relation/RelationCreateAction.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Relation/RelationCreateAction.cs new file mode 100644 index 00000000..61f49ca2 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Relation/RelationCreateAction.cs @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Application.Actions.Base; +using DsmSuite.DsmViewer.Application.Actions.Management; +using DsmSuite.DsmViewer.Application.Interfaces; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Actions.Relation +{ + public class RelationCreateAction : IAction + { + private readonly IDsmModel _model; + private readonly IActionContext _actionContext; + private IDsmRelation _relation; + private readonly IDsmElement _consumer; + private readonly IDsmElement _provider; + private readonly string _type; + private readonly int _weight; + + public const ActionType RegisteredType = ActionType.RelationCreate; + + public RelationCreateAction(IDsmModel model, IActionContext context, IReadOnlyDictionary data) + { + _model = model; + _actionContext = context; + if (_model != null && data != null) + { + ActionReadOnlyAttributes attributes = new ActionReadOnlyAttributes(_model, data); + + _relation = attributes.GetRelation(nameof(_relation)); + _consumer = attributes.GetRelationConsumer(nameof(_relation)); + _provider = attributes.GetRelationProvider(nameof(_relation)); + _type = attributes.GetString(nameof(_type)); + _weight = attributes.GetInt(nameof(_weight)); + } + } + + public RelationCreateAction(IDsmModel model, int consumerId, int providerId, string type, int weight) + { + _model = model; + _consumer = model.GetElementById(consumerId); + _provider = model.GetElementById(providerId); + _type = type; + _weight = weight; + } + + public ActionType Type => RegisteredType; + public string Title => "Create relation"; + public string Description => $"consumer={_consumer.Fullname} provider={_provider.Fullname} type={_type} weight={_weight}"; + + public object Do() + { + return _model.AddRelation(_consumer, _provider, _type, _weight, null); + } + + public void Undo() + { + _model.RemoveRelation(_relation.Id); + } + + public bool IsValid() + { + return (_model != null) && + (_relation != null) && + (_provider != null) && + (_provider != null) && + (_type != null); + } + + public IReadOnlyDictionary Data + { + get + { + ActionAttributes attributes = new ActionAttributes(); + attributes.SetInt(nameof(_relation), _relation.Id); + attributes.SetInt(nameof(_consumer), _consumer.Id); + attributes.SetInt(nameof(_provider), _provider.Id); + attributes.SetString(nameof(_type), _type); + attributes.SetInt(nameof(_weight), _weight); + return attributes.Data; + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Relation/RelationDeleteAction.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Relation/RelationDeleteAction.cs new file mode 100644 index 00000000..62a69183 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Relation/RelationDeleteAction.cs @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Application.Actions.Base; +using DsmSuite.DsmViewer.Application.Actions.Management; +using DsmSuite.DsmViewer.Application.Interfaces; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Actions.Relation +{ + public class RelationDeleteAction : IAction + { + private readonly IDsmModel _model; + private readonly IActionContext _actionContext; + private readonly IDsmRelation _relation; + private readonly IDsmElement _consumer; + private readonly IDsmElement _provider; + + public const ActionType RegisteredType = ActionType.RelationDelete; + + public RelationDeleteAction(IDsmModel model, IActionContext context, IReadOnlyDictionary data) + { + _model = model; + _actionContext = context; + if (_model != null && data != null) + { + ActionReadOnlyAttributes attributes = new ActionReadOnlyAttributes(_model, data); + + _relation = attributes.GetRelation(nameof(_relation)); + _consumer = attributes.GetRelationConsumer(nameof(_relation)); + _provider = attributes.GetRelationProvider(nameof(_relation)); + } + } + + public RelationDeleteAction(IDsmModel model, IDsmRelation relation) + { + _model = model; + _relation = relation; + _consumer = model.GetElementById(_relation.Consumer.Id); + _provider = model.GetElementById(_relation.Provider.Id); + } + + public ActionType Type => RegisteredType; + public string Title => "Delete relation"; + public string Description => $"consumer={_consumer.Fullname} provider={_provider.Fullname} type={_relation.Type}"; + + public object Do() + { + _model.RemoveRelation(_relation.Id); + return null; + } + + public void Undo() + { + _model.UnremoveRelation(_relation.Id); + } + + public bool IsValid() + { + return (_model != null) && + (_relation != null) && + (_consumer != null) && + (_provider != null); + } + + public IReadOnlyDictionary Data + { + get + { + ActionAttributes attributes = new ActionAttributes(); + attributes.SetInt(nameof(_relation), _relation.Id); + return attributes.Data; + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Snapshot/MakeSnapshotAction.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Snapshot/MakeSnapshotAction.cs new file mode 100644 index 00000000..dddd3a24 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Actions/Snapshot/MakeSnapshotAction.cs @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Application.Actions.Base; +using DsmSuite.DsmViewer.Application.Actions.Management; +using DsmSuite.DsmViewer.Application.Interfaces; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Actions.Snapshot +{ + public class MakeSnapshotAction : IAction + { + private readonly IDsmModel _model; + private readonly IActionContext _actionContext; + private readonly string _name; + + public const ActionType RegisteredType = ActionType.Snapshot; + + /// + /// MakeSnapshotAction creates a bookmark in the do/undo stacks. + /// Neither Do() nor Undo() has any effect. + /// + public MakeSnapshotAction(IDsmModel model, IActionContext context, IReadOnlyDictionary data) + { + _model = model; + _actionContext = context; + + if (_model != null && data != null) + { + ActionReadOnlyAttributes attributes = new ActionReadOnlyAttributes(_model, data); + _name = attributes.GetString(nameof(_name)); + } + } + + public MakeSnapshotAction(IDsmModel model, string name) + { + _model = model; + _name = name; + } + + public ActionType Type => RegisteredType; + public string Title => "Make snapshot"; + public string Description => $"name={_name}"; + public string Name => _name; + + public object Do() + { + return null; + } + + public void Undo() + { + } + + public bool IsValid() + { + return _model != null && _name != null; + } + + public IReadOnlyDictionary Data + { + get + { + ActionAttributes attributes = new ActionAttributes(); + attributes.SetString(nameof(_name), _name); + return attributes.Data; + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/ApplicationClasses.cd b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/ApplicationClasses.cd new file mode 100644 index 00000000..bb7c1436 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/ApplicationClasses.cd @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + ApE2RAoAJoyQBAiDABIbREkhoIhohYCFRlAgBIUAQIA= + Core\DsmApplication.cs + + + + + + + + + + + + + + + AAIQAAQABACABASAAIAgEAIBAAAAAAAAECACAAFAAIA= + Actions\Management\ActionManager.cs + + + + + + + AAAAAAAAAAAAAAQAAAAAhAAAEAAAAAAAAAAAAEAAAAA= + Actions\Management\ActionStore.cs + + + + + + AAAAAAIAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAA= + Metrics\DsmMetrics.cs + + + + + + + + + + + AAAABAAAAAAAAABAAgAACAAAgAEggACAAAAAAAQAAAA= + Queries\DsmQueries.cs + + + + + + + + + + + + + AAIAAAAAAACAAAQAAIAAAAAAAAAAAAAAACAAAABAAAA= + Actions\Management\IActionManager.cs + + + + + + AAAAAAAAAAAgBEAAAAgAAAAACAAAABAAAQAAAAAAAAA= + Interfaces\IAction.cs + + + + + + + + \ No newline at end of file diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Core/DsmApplication.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Core/DsmApplication.cs new file mode 100644 index 00000000..62b5e8b4 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Core/DsmApplication.cs @@ -0,0 +1,440 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using System.Reflection; +using DsmSuite.Analyzer.Model.Core; +using DsmSuite.Common.Util; +using DsmSuite.DsmViewer.Application.Actions.Element; +using DsmSuite.DsmViewer.Application.Actions.Filtering; +using DsmSuite.DsmViewer.Application.Actions.Management; +using DsmSuite.DsmViewer.Application.Actions.Relation; +using DsmSuite.DsmViewer.Application.Actions.Snapshot; +using DsmSuite.DsmViewer.Application.Import.Common; +using DsmSuite.DsmViewer.Application.Import.Dsi; +using DsmSuite.DsmViewer.Application.Interfaces; +using DsmSuite.DsmViewer.Application.Metrics; +using DsmSuite.DsmViewer.Application.Queries; +using DsmSuite.DsmViewer.Application.Sorting; +using DsmSuite.DsmViewer.Model.Core; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Core +{ + public class DsmApplication : IDsmApplication + { + private IDsmModel _dsmModel; + private ActionManager _actionManager; + private readonly ActionStore _actionStore; + private readonly DsmQueries _queries; + private readonly DsmMetrics _metrics; + + public event EventHandler Modified; + public event EventHandler ActionPerformed; + + public DsmApplication(IDsmModel dsmModel) + { + _dsmModel = dsmModel; + _queries = new DsmQueries(dsmModel); + _metrics = new DsmMetrics(); + _actionStore = new ActionStore(); + _actionManager = new ActionManager(); + _actionManager.ActionPerformed += OnActionPerformed; + } + + + /// + /// Set a new model and actionManager. The current model and actionmanager are + /// discarded. + /// + private void LoadModel(IDsmModel model) { + _dsmModel = model; + _actionManager = new ActionManager(); + _actionStore.LoadFromModel(_actionManager, _dsmModel); + _actionManager.ActionPerformed += OnActionPerformed; + IsModified = false; + Modified?.Invoke(this, IsModified); + } + + + private void OnActionPerformed(object sender, EventArgs e) + { + ActionPerformed?.Invoke(this, e); + IsModified = true; + Modified?.Invoke(this, IsModified); + } + + public bool CanUndo() + { + return _actionManager.CanUndo(); + } + + public string GetUndoActionDescription() + { + return _actionManager.GetCurrentUndoAction()?.Description; + } + + public void Undo() + { + _actionManager.Undo(); + } + public void GotoAction(IAction action) + { + _actionManager.Goto(action); + } + + public bool CanRedo() + { + return _actionManager.CanRedo(); + } + + public string GetRedoActionDescription() + { + return _actionManager.GetCurrentRedoAction()?.Description; + } + + public void Redo() + { + _actionManager.Redo(); + } + + public async Task AsyncImportDsiModel(string dsiFilename, string dsmFilename, + bool autoPartition, bool compressDsmFile, IProgress progress) + { + IDsmModel model = await Task.Run( () => + ImportDsiModel(dsiFilename, dsmFilename, autoPartition, compressDsmFile, progress) ); + LoadModel(model); + } + + + /// + /// Create a model by loading data from dsiFilename, save it as dsm to dsmFilename + /// and return the model. + /// + private IDsmModel ImportDsiModel(string dsiFilename, string dsmFilename, + bool autoPartition, bool compressDsmFile, IProgress progress) + { + Assembly assembly = Assembly.GetEntryAssembly(); + + //---- Read file into DsiModel + DsiModel dsiModel = new DsiModel("Builder", new List(), assembly); + dsiModel.Load(dsiFilename, progress); + + //---- Create DsmModel from DsiModel + IDsmModel model = new DsmModel("Viewer", assembly); + DsiImporter importer = new DsiImporter(dsiModel, model, autoPartition); + importer.Import(progress); + + //---- Save DsmModel + model.SaveModel(dsmFilename, compressDsmFile, progress); + + return model; + } + + + // Changed 2026-07 for CSharpCodeAnalyst: AsyncImportSqlModel / ImportSqlModel and the + // SqlImporter they used were removed. The .sql import is not offered by the embedded + // matrix view, and dropping it removes the Dapper and Microsoft.Data.Sqlite dependencies. + + + public async Task AsyncOpenModel(string dsmFilename, Progress progress) + { + IDsmModel model = new DsmModel("Viewer", Assembly.GetExecutingAssembly()); + await Task.Run(() => model.LoadModel(dsmFilename, progress)); + LoadModel(model); + } + + + public async Task AsyncSaveModel(string dsmFilename, Progress progress) + { + _actionStore.SaveToModel(_actionManager, _dsmModel); + await Task.Run(() => _dsmModel.SaveModel(dsmFilename, _dsmModel.IsCompressed, progress)); + IsModified = false; + Modified?.Invoke(this, IsModified); + } + + + public IDsmElement RootElement => _dsmModel.RootElement; + + public bool IsModified { get; private set; } + + public IEnumerable GetElementConsumers(IDsmElement element) + { + return _queries.FindConsumersOf(element); + } + + public IEnumerable GetElementProvidedElements(IDsmElement element) + { + return _queries.FindElementsProvidedBy(element); + } + + public IEnumerable GetElementProviders(IDsmElement element) + { + return _queries.FindProvidersFor(element); + } + + public IEnumerable FindResolvedRelations(IDsmElement consumer, IDsmElement provider) + { + return _queries.FindRelations(consumer, provider); + } + + public IEnumerable FindRelations(IDsmElement consumer, IDsmElement provider) + { + return _dsmModel.FindRelations(consumer, provider); + } + + public int GetRelationCount(IDsmElement consumer, IDsmElement provider) + { + return _dsmModel.GetRelationCount(consumer, provider); + } + + public IEnumerable FindIngoingRelations(IDsmElement element) + { + return _queries.FindConsumingRelations(element); + } + + public IEnumerable FindOutgoingRelations(IDsmElement element) + { + return _queries.FindProvidingRelations(element); + } + + public IEnumerable FindInternalRelations(IDsmElement element) + { + return _queries.FindInternalRelations(element); + } + + public IEnumerable FindExternalRelations(IDsmElement element) + { + return _queries.FindExternalRelations(element); + } + + public IEnumerable GetRelationProviders(IDsmElement consumer, IDsmElement provider) + { + return _queries.FindRelationProviders(consumer, provider); + } + + public IEnumerable GetRelationConsumers(IDsmElement consumer, IDsmElement provider) + { + return _queries.FindRelationConsumers(consumer, provider); + } + + public int GetHierarchicalCycleCount(IDsmElement element) + { + return _dsmModel.GetHierarchicalCycleCount(element); + } + + public int GetSystemCycleCount(IDsmElement element) + { + return _dsmModel.GetSystemCycleCount(element); + } + + public IDsmElement NextSibling(IDsmElement element) + { + return _dsmModel.NextSibling(element); + } + + public IDsmElement PreviousSibling(IDsmElement element) + { + return _dsmModel.PreviousSibling(element); + } + + public bool IsFirstChild(IDsmElement element) + { + return _dsmModel.PreviousSibling(element) == null; + } + + public bool IsLastChild(IDsmElement element) + { + return _dsmModel.NextSibling(element) == null; + } + + public bool HasChildren(IDsmElement element) + { + return element?.Children.Count > 0; + } + + public void Sort(IDsmElement element, string algorithm) + { + ElementSortAction action = new ElementSortAction(_dsmModel, element, algorithm); + _actionManager.Execute(action); + } + + public void SortRecursively(IDsmElement element, string algorithm) + { + IAction action = new ElementSortRecursiveAction(_dsmModel, element, algorithm); + _actionManager.Execute(action); + } + + public IEnumerable GetSupportedSortAlgorithms() + { + return SortAlgorithmFactory.GetSupportedAlgorithms(); + } + + public void MoveUp(IDsmElement element) + { + ElementMoveUpAction action = new ElementMoveUpAction(_dsmModel, element); + _actionManager.Execute(action); + } + + public void MoveDown(IDsmElement element) + { + ElementMoveDownAction action = new ElementMoveDownAction(_dsmModel, element); + _actionManager.Execute(action); + } + + public IEnumerable GetElementTypes() + { + return _dsmModel.GetElementTypes(); + } + + public int GetDependencyWeight(IDsmElement consumer, IDsmElement provider) + { + return _dsmModel.GetDependencyWeight(consumer, provider); + } + + public int GetDirectDependencyWeight(IDsmElement consumer, IDsmElement provider) + { + return _dsmModel.GetDirectDependencyWeight(consumer, provider); + } + + public CycleType IsCyclicDependency(IDsmElement consumer, IDsmElement provider) + { + return _dsmModel.IsCyclicDependency(consumer, provider); + } + + public IList SearchElements(string searchText, IDsmElement searchInElement, bool caseSensitive, string elementTypeFilter, bool markMatchingElements) + { + return _dsmModel.SearchElements(searchText, searchInElement, caseSensitive, elementTypeFilter, markMatchingElements); + } + + public IDsmElement GetElementByFullname(string text) + { + return _dsmModel.GetElementByFullname(text); + } + + public IDsmElement CreateElement(string name, string type, IDsmElement parent, int index) + { + ElementCreateAction action = new ElementCreateAction(_dsmModel, name, type, parent, index); + return _actionManager.Execute(action) as IDsmElement; + } + + public void DeleteElement(IDsmElement element) + { + ElementDeleteAction action = new ElementDeleteAction(_dsmModel, element); + _actionManager.Execute(action); + } + + public void ChangeElementName(IDsmElement element, string name) + { + ElementChangeNameAction action = new ElementChangeNameAction(_dsmModel, element, name); + _actionManager.Execute(action); + } + + public void ChangeElementType(IDsmElement element, string type) + { + ElementChangeTypeAction action = new ElementChangeTypeAction(_dsmModel, element, type); + _actionManager.Execute(action); + } + + public void ChangeElementParent(IDsmElement element, IDsmElement newParent, int index) + { + if (_dsmModel.IsChangeElementParentAllowed(element, newParent)) + { + ElementChangeParentAction action = new ElementChangeParentAction(_dsmModel, element, newParent, index); + _actionManager.Execute(action); + } + } + + public void CutElement(IDsmElement element) + { + ElementCutAction action = new ElementCutAction(_dsmModel, element, _actionManager.GetContext()); + _actionManager.Execute(action); + } + + public void CopyElement(IDsmElement element) + { + ElementCopyAction action = new ElementCopyAction(_dsmModel, element, _actionManager.GetContext()); + _actionManager.Execute(action); + } + + public void PasteElement(IDsmElement newParent, int index) + { + ElementPasteAction action = new ElementPasteAction(_dsmModel, newParent, index, _actionManager.GetContext()); + _actionManager.Execute(action); + } + + + public IDsmRelation CreateRelation(IDsmElement consumer, IDsmElement provider, string type, int weight) + { + RelationCreateAction action = new RelationCreateAction(_dsmModel, consumer.Id, provider.Id, type, weight); + return _actionManager.Execute(action) as IDsmRelation; + } + + public void DeleteRelation(IDsmRelation relation) + { + RelationDeleteAction action = new RelationDeleteAction(_dsmModel, relation); + _actionManager.Execute(action); + } + + public void ChangeRelationType(IDsmRelation relation, string type) + { + RelationChangeTypeAction action = new RelationChangeTypeAction(_dsmModel, relation, type); + _actionManager.Execute(action); + } + + public void ChangeRelationWeight(IDsmRelation relation, int weight) + { + RelationChangeWeightAction action = new RelationChangeWeightAction(_dsmModel, relation, weight); + _actionManager.Execute(action); + } + + public IEnumerable GetRelationTypes() + { + return _dsmModel.GetRelationTypes(); + } + + public void ShowElementDetail(IDsmElement provider, IDsmElement consumer) + { + IAction action = new ShowElementDetailAction(_dsmModel, provider, consumer); + _actionManager.Execute(action); + } + + public void ShowElementContext(IDsmElement provider) + { + IAction action = new ShowElementContextAction(_dsmModel, provider); + _actionManager.Execute(action); + } + + public void MakeSnapshot(string description) + { + MakeSnapshotAction action = new MakeSnapshotAction(_dsmModel, description); + _actionManager.Execute(action); + } + + public IEnumerable GetActions() + { + return _actionManager.GetActionsInReverseChronologicalOrder(); + } + + public IEnumerable GetAllActions() + { + return _actionManager.GetActionsInChronologicalOrder() + .Concat(_actionManager.GetRedoActionsInChronologicalOrder()); + } + + public void ClearActions() + { + _actionManager.Clear(); + _dsmModel.ClearActions(); + IsModified = true; + Modified?.Invoke(this, IsModified); + } + + public int GetElementSize(IDsmElement element) + { + return _metrics.GetElementSize(element); + } + + public int GetElementCount() + { + return _dsmModel.GetElementCount(); + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/DsmSuite.DsmViewer.Application.csproj b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/DsmSuite.DsmViewer.Application.csproj new file mode 100644 index 00000000..6b75cf5b --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/DsmSuite.DsmViewer.Application.csproj @@ -0,0 +1,17 @@ + + + net10.0-windows + Library + enable + + + + + + + + + + + + diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Import/Common/ImporterBase.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Import/Common/ImporterBase.cs new file mode 100644 index 00000000..bb35cc62 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Import/Common/ImporterBase.cs @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.Common.Util; +using DsmSuite.DsmViewer.Application.Sorting; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Import.Common +{ + public class ImporterBase + { + private readonly IDsmModel _dsmModel; + private int _progressPercentage; + + public ImporterBase(IDsmModel dsmModel) + { + _dsmModel = dsmModel; + _dsmModel.Clear(); + } + + protected void Partition(IProgress progress) + { + int totalElements = _dsmModel.GetElementCount(); + int progressedElements = 0; + Partition(progress, _dsmModel.RootElement, totalElements, ref progressedElements); + } + + protected void Partition(IProgress progress, IDsmElement element, int totalElements, ref int progressedElements) + { + ISortAlgorithm algorithm = SortAlgorithmFactory.CreateAlgorithm(_dsmModel, element, PartitionSortAlgorithm.AlgorithmName); + _dsmModel.ReorderChildren(element, algorithm.Sort()); + progressedElements++; + UpdateProgress(progress, "Partition elements", totalElements, progressedElements); + + foreach (IDsmElement child in element.Children) + { + Partition(progress, child, totalElements, ref progressedElements); + } + } + + protected void FinalizeImport(IProgress progress) + { + _dsmModel.AssignElementOrder(); + } + + protected void UpdateProgress(IProgress progress, string progressActionText, int totalItemCount, int progressedItemCount) + { + if (progress != null) + { + int currentProgressPercentage = 0; + if (totalItemCount > 0) + { + currentProgressPercentage = progressedItemCount * 100 / totalItemCount; + } + + if (_progressPercentage != currentProgressPercentage) + { + _progressPercentage = currentProgressPercentage; + + ProgressInfo progressInfoInfo = new ProgressInfo + { + ActionText = progressActionText, + TotalItemCount = totalItemCount, + CurrentItemCount = progressedItemCount, + ItemType = "items", + Percentage = currentProgressPercentage, + Done = totalItemCount == progressedItemCount + }; + + progress.Report(progressInfoInfo); + } + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Import/Dsi/DsiImporter.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Import/Dsi/DsiImporter.cs new file mode 100644 index 00000000..3ae2c4e5 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Import/Dsi/DsiImporter.cs @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.Analyzer.Model.Interface; +using DsmSuite.Common.Model.Interface; +using DsmSuite.Common.Util; +using DsmSuite.DsmViewer.Application.Import.Common; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Import.Dsi +{ + public class DsiImporter : ImporterBase + { + private readonly IDsiModel _dsiModel; + private readonly IDsmModel _dsmModel; + private readonly bool _autoPartition; + private readonly Dictionary _dsiToDsmMapping; + private int _totalItemCount; + private int _progressedItemCount; + + public DsiImporter(IDsiModel dsiModel, IDsmModel dsmModel, bool autoPartition) : base(dsmModel) + { + _dsiModel = dsiModel; + _dsmModel = dsmModel; + _autoPartition = autoPartition; + _dsiToDsmMapping = new Dictionary(); + + _totalItemCount = _dsiModel.GetElements().Count() + _dsiModel.GetRelations().Count(); + } + + public void Import(IProgress progress) + { + ImportMetaDataItems(); + ImportElements(progress); + ImportRelations(progress); + + if (_autoPartition) + { + Partition(progress); + } + + FinalizeImport(progress); + } + + private void ImportMetaDataItems() + { + foreach (string groupName in _dsiModel.GetMetaDataGroups()) + { + foreach (IMetaDataItem metaDatItem in _dsiModel.GetMetaDataGroupItems(groupName)) + { + ImportMetaDataItem(groupName, metaDatItem.Name, metaDatItem.Value); + } + } + } + + private void ImportElements(IProgress progress) + { + foreach (IDsiElement dsiElement in _dsiModel.GetElements()) + { + ImportElement(dsiElement); + _progressedItemCount++; + UpdateProgress(progress, "Importing dsi model", _totalItemCount, _progressedItemCount); + } + } + + private void ImportElement(IDsiElement dsiElement) + { + IDsmElement parent = null; + + char[] trimCharacters = { ' ', '.' }; + string dsiElementName = dsiElement.Name.TrimStart(trimCharacters); + ElementName elementName = new ElementName(); + foreach (string name in new ElementName(dsiElementName).NameParts) + { + elementName.AddNamePart(name); + + bool isElementLeaf = (dsiElementName == elementName.FullName); + + if (isElementLeaf) + { + IDsmElement element = ImportElement(elementName.FullName, name, dsiElement.Type, parent, dsiElement.Properties); + parent = element; + _dsiToDsmMapping[dsiElement.Id] = element.Id; + + Logger.LogInfo($"Import leaf element dsiId={dsiElement.Id} dsiName={dsiElement.Name} dsmId={element.Id} dsmName={elementName.FullName}"); + } + else + { + IDsmElement element = ImportElement(elementName.FullName, name, "", parent, null); + parent = element; + + Logger.LogInfo($"Import non leaf element dsiId={dsiElement.Id} dsiName={dsiElement.Name} dsmName={elementName.FullName}"); + } + } + } + + private void ImportRelations(IProgress progress) + { + foreach (IDsiRelation dsiRelation in _dsiModel.GetRelations()) + { + ImportRelation(dsiRelation); + _progressedItemCount++; + UpdateProgress(progress, "Importing dsi model", _totalItemCount, _progressedItemCount); + } + } + + private void ImportRelation(IDsiRelation dsiRelation) + { + Logger.LogInfo($"Import relation consumerId={dsiRelation.ConsumerId} providerId={dsiRelation.ProviderId}"); + + if (_dsiToDsmMapping.ContainsKey(dsiRelation.ConsumerId) && _dsiToDsmMapping.ContainsKey(dsiRelation.ProviderId)) + { + int consumerId = _dsiToDsmMapping[dsiRelation.ConsumerId]; + int providerId = _dsiToDsmMapping[dsiRelation.ProviderId]; + string type = dsiRelation.Type; + int weight = dsiRelation.Weight; + + if (consumerId != providerId) + { + ImportRelation(consumerId, providerId, type, weight, dsiRelation.Properties); + } + } + else + { + Logger.LogError($"Could not find consumer or provider of relation consumer={dsiRelation.ConsumerId} provider={dsiRelation.ProviderId}"); + } + } + + private IMetaDataItem ImportMetaDataItem(string group, string name, string value) + { + return _dsmModel.AddMetaData(group, name, value); + } + + private IDsmElement ImportElement(string fullname, string name, string type, IDsmElement parent, IDictionary properties) + { + IDsmElement element = _dsmModel.GetElementByFullname(fullname); + if (element == null) + { + int? parentId = parent?.Id; + element = _dsmModel.AddElement(name, type, parentId, 0, properties); + } + return element; + } + + private IDsmRelation ImportRelation(int consumerId, int providerId, string type, int weight, IDictionary properties) + { + IDsmElement consumer = _dsmModel.GetElementById(consumerId); + IDsmElement provider = _dsmModel.GetElementById(providerId); + + return _dsmModel.AddRelation(consumer, provider, type, weight, properties); + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Interfaces/ActionType.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Interfaces/ActionType.cs new file mode 100644 index 00000000..1cd3b314 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Interfaces/ActionType.cs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +namespace DsmSuite.DsmViewer.Application.Interfaces +{ + /// + /// The enumeration of all available user actions (i.e. IAction implementors). + /// Every enumeration tag should be equal to the name of its implementing class. + /// + public enum ActionType + { + ElementChangeName, + ElementChangeParent, + ElementChangeType, + ElementCreate, + ElementDelete, + ElementMoveUp, + ElementMoveDown, + ElementSort, + ElementSortRecursive, + ElementCopy, + ElementCut, + ElementPaste, + + RelationChangeType, + RelationChangeWeight, + RelationCreate, + RelationDelete, + + ShowElementDetail, + ShowElementContext, + + Snapshot + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Interfaces/IAction.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Interfaces/IAction.cs new file mode 100644 index 00000000..1e30096a --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Interfaces/IAction.cs @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +namespace DsmSuite.DsmViewer.Application.Interfaces +{ + /// + /// An action the user can perform in the application. + /// Actions execute on the data in the property. This data is injected + /// into the constructor.
+ /// When the model is saved, the property is saved with the action.
+ /// When loading the model again, the action is re-created using a + /// constructor(IDsmModel m, IActionContext c, IReadOnlyDictionary<string,string> data). + ///
+ public interface IAction + { + /// + /// The unique type for this action. + /// + /// todo only used by ActionStore for impex. Implement locally there and remove here. + ActionType Type { get; } + + /// + /// String that identifies this action in menus and action lists (e.g. undo/redo). + /// This must be the same for all instances of the action. + /// + string Title { get; } + + /// + /// String that describes the details of this particular action for informative purposes + /// (e.g. in undo/redo). + /// This usually returns a different text for different instances of the action, based + /// on the contents of . + /// + string Description { get; } + + /// + /// Perform this action using the data stored in Data.
+ /// This usually returns null, except for actions that are expected by the application to + /// return some useful object, like create actions. + ///
+ /// Some relevant object or null + object Do(); + + /// + /// Undo the result of Do(). + /// + void Undo(); + + /// + /// Return true iff this action has all the data it needs to be done/undone. + /// + bool IsValid(); + + /// + /// A key->value map of the data this action needs to be performed, injected into the + /// constructor. + /// The implementing class can determine the contents and format of Data itself, but + /// ActionAttributes and ActionReadOnlyAttributes provide convenience + /// functions for this. + /// + IReadOnlyDictionary Data { get; } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Interfaces/IDsmApplication.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Interfaces/IDsmApplication.cs new file mode 100644 index 00000000..7d6890e9 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Interfaces/IDsmApplication.cs @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.Common.Util; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Interfaces +{ + public interface IDsmApplication + { + event EventHandler Modified; + event EventHandler ActionPerformed; + + Task AsyncImportDsiModel(string dsiFilename, string dsmFilename, bool autoPartition, bool compressDsmFile, IProgress progress); + // Changed 2026-07 for CSharpCodeAnalyst: AsyncImportSqlModel removed together with SqlImporter. + + Task AsyncOpenModel(string dsmFilename, Progress progress); + Task AsyncSaveModel(string dsmFilename, Progress progress); + bool IsModified { get; } + bool CanUndo(); + string GetUndoActionDescription(); + void Undo(); + void GotoAction(IAction action); + bool CanRedo(); + string GetRedoActionDescription(); + void Redo(); + IDsmElement RootElement { get; } + IEnumerable GetElementConsumers(IDsmElement element); + IEnumerable GetElementProvidedElements(IDsmElement element); + IEnumerable GetElementProviders(IDsmElement element); + IEnumerable GetRelationProviders(IDsmElement consumer, IDsmElement provider); + IEnumerable GetRelationConsumers(IDsmElement consumer, IDsmElement provider); + IEnumerable FindResolvedRelations(IDsmElement consumer, IDsmElement provider); + IEnumerable FindRelations(IDsmElement consumer, IDsmElement provider); + int GetRelationCount(IDsmElement consumer, IDsmElement provider); + IEnumerable FindIngoingRelations(IDsmElement element); + IEnumerable FindOutgoingRelations(IDsmElement element); + IEnumerable FindInternalRelations(IDsmElement element); + IEnumerable FindExternalRelations(IDsmElement element); + int GetHierarchicalCycleCount(IDsmElement element); + int GetSystemCycleCount(IDsmElement element); + IDsmElement NextSibling(IDsmElement element); + IDsmElement PreviousSibling(IDsmElement element); + bool HasChildren(IDsmElement element); + void Sort(IDsmElement element, string algorithm); + void SortRecursively(IDsmElement element, string algorithm); + IEnumerable GetSupportedSortAlgorithms(); + void MoveUp(IDsmElement element); + void MoveDown(IDsmElement element); + IEnumerable GetElementTypes(); + + + int GetDependencyWeight(IDsmElement consumer, IDsmElement provider); + CycleType IsCyclicDependency(IDsmElement consumer, IDsmElement provider); + IList SearchElements(string searchText, IDsmElement searchInElement, bool caseSensitive, string elementTypeFilter, bool markMatchingElements); + IDsmElement GetElementByFullname(string fullname); + IDsmElement CreateElement(string name, string type, IDsmElement parent, int index); + void DeleteElement(IDsmElement element); + void ChangeElementName(IDsmElement element, string name); + void ChangeElementType(IDsmElement element, string type); + void ChangeElementParent(IDsmElement element, IDsmElement newParent, int index); + void CutElement(IDsmElement element); + void CopyElement(IDsmElement element); + void PasteElement(IDsmElement newParent, int index); + + IDsmRelation CreateRelation(IDsmElement consumer, IDsmElement provider, string type, int weight); + void DeleteRelation(IDsmRelation relation); + void ChangeRelationType(IDsmRelation relation, string type); + void ChangeRelationWeight(IDsmRelation relation, int weight); + IEnumerable GetRelationTypes(); + void MakeSnapshot(string name); + + /// + /// Return the undoable actions. + /// + IEnumerable GetActions(); + void ClearActions(); + /// + /// Return the undoable and redoable actions in a single order. + /// + IEnumerable GetAllActions(); + + int GetElementSize(IDsmElement element); + int GetElementCount(); + void ShowElementDetail(IDsmElement consumer, IDsmElement provider); + void ShowElementContext(IDsmElement provider); + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Interfaces/IMultiAction.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Interfaces/IMultiAction.cs new file mode 100644 index 00000000..202a6fd2 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Interfaces/IMultiAction.cs @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DsmSuite.DsmViewer.Application.Interfaces +{ + /// + /// An action that can contain a sequence of sub-actions. + /// + public interface IMultiAction : IAction + { + IEnumerable Actions { get; } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Interfaces/WeightedElement.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Interfaces/WeightedElement.cs new file mode 100644 index 00000000..8c54fb81 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Interfaces/WeightedElement.cs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Interfaces +{ + public record WeightedElement(IDsmElement Element, int weight); +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Metrics/DsmMetrics.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Metrics/DsmMetrics.cs new file mode 100644 index 00000000..ed37dfa7 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Metrics/DsmMetrics.cs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Metrics +{ + public class DsmMetrics + { + public int GetElementSize(IDsmElement element) + { + int count = 0; + CountChildren(element, ref count); + return count; + } + + private void CountChildren(IDsmElement element, ref int count) + { + count++; + + foreach (IDsmElement child in element.Children) + { + CountChildren(child, ref count); + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Queries/DsmQueries.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Queries/DsmQueries.cs new file mode 100644 index 00000000..c769fe43 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Queries/DsmQueries.cs @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using System.Collections.Generic; +using System.Linq; +using DsmSuite.DsmViewer.Application.Interfaces; +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Queries +{ + /// + /// Contains some but not all queries the application executes on the model + /// + /// TODO Can this class be made more useful? + /// DsmApplication does some queries as well and dispatches others here. Can we make this class more useful + /// by forwarding once and dispatching here? + public class DsmQueries + { + + private readonly IDsmModel _model; + public DsmQueries(IDsmModel model) + { + _model = model; + } + + /// + /// Return sub-elements of element that are providers for elements outside + /// of element. The result is ordered by provider name. + /// + public IEnumerable FindElementsProvidedBy(IDsmElement element) + { + var elements = _model.FindIngoingRelations(element) + .OrderBy(x => x.Provider.Fullname) + .GroupBy(x => x.Provider.Fullname) + .Select( g => new WeightedElement(g.First().Provider, g.Sum(x => x.Weight)) ) + .ToList(); + + return elements; + } + + /// + /// Return elements outside of element that are providers for elements within + /// element. The result is ordered by provider name. + /// + public IEnumerable FindProvidersFor(IDsmElement element) + { + var elements = _model.FindOutgoingRelations(element) + .OrderBy(x => x.Provider.Fullname) + .GroupBy(x => x.Provider.Fullname) + .Select( g => new WeightedElement(g.First().Provider, g.Sum(x => x.Weight)) ) + .ToList(); + + return elements; + } + + /// + /// Return elements outside of element that are consumer of elements within + /// element. The result is ordered by consumer name. + /// + public IEnumerable FindConsumersOf(IDsmElement element) + { + var elements = _model.FindIngoingRelations(element) + .OrderBy(x => x.Consumer.Fullname) + .GroupBy(x => x.Consumer.Fullname) + .Select( g => new WeightedElement(g.First().Consumer, g.Sum(x => x.Weight)) ) + .ToList(); + + return elements; + } + + /// + /// Return the consumers in the relations between (sub-elements of) consumer and + /// provider. The result is ordered by consumer name. + /// + public IEnumerable FindRelationConsumers(IDsmElement consumer, IDsmElement provider) + { + var elements = _model.FindRelations(consumer, provider) + .OrderBy(x => x.Consumer.Fullname) + .GroupBy(x => x.Consumer.Fullname) + .Select(g => new WeightedElement(g.First().Consumer, g.Sum(x => x.Weight))) + .ToList(); + + return elements; + } + + /// + /// Return the providers in the relations between (sub-elements of) consumer and + /// provider. The result is ordered by provider name. + /// + public IEnumerable FindRelationProviders(IDsmElement consumer, IDsmElement provider) + { + var elements = _model.FindRelations(consumer, provider) + .OrderBy(x => x.Provider.Fullname) + .GroupBy(x => x.Provider.Fullname) + .Select(g => new WeightedElement(g.First().Provider, g.Sum(x => x.Weight))) + .ToList(); + + return elements; + } + + /// + /// Return the relations between (sub-elements of) consumer and + /// provider. The result is ordered by provider name, consumer name. + /// + public IEnumerable FindRelations(IDsmElement consumer, IDsmElement provider) + { + var relations = _model.FindRelations(consumer, provider) + .OrderBy(x => x.Provider.Fullname) + .ThenBy(x => x.Consumer.Fullname) + .ToList(); + return relations; + } + + /// + /// Return the relations consuming (a sub-elements of) element. + /// + public IEnumerable FindConsumingRelations(IDsmElement element) + { + return _model.FindIngoingRelations(element); + } + + /// + /// Return the relations providing to (a sub-elements of) element. + /// + public IEnumerable FindProvidingRelations(IDsmElement element) + { + return _model.FindOutgoingRelations(element); + } + + /// + /// Return the relations between sub-elements of element. + /// + public IEnumerable FindInternalRelations(IDsmElement element) + { + return _model.FindInternalRelations(element); + } + + /// + /// Return the relations between (a sub-element of) element and an element + /// outside of element. + /// + public IEnumerable FindExternalRelations(IDsmElement element) + { + return _model.FindExternalRelations(element); + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/AlphabeticalSortAlgorithm.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/AlphabeticalSortAlgorithm.cs new file mode 100644 index 00000000..26f0b81b --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/AlphabeticalSortAlgorithm.cs @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Model.Interfaces; +using System.Diagnostics; + +namespace DsmSuite.DsmViewer.Application.Sorting +{ + public class AlphabeticalSortAlgorithm : ISortAlgorithm + { + private readonly IDsmModel _model; + private readonly IDsmElement _element; + + public const string AlgorithmName = "Alphabetical"; + + public AlphabeticalSortAlgorithm(object[] args) + { + Debug.Assert(args.Length == 2); + _model = args[0] as IDsmModel; + Debug.Assert(_model != null); + _element = args[1] as IDsmElement; + Debug.Assert(_element != null); + } + + public SortResult Sort() + { + SortResult vector = new SortResult(_element.Children.Count); + + if (_element.Children.Count > 1) + { + List newOrder = _element.Children.OrderBy(x => x.Name).Select(x => x.Id).ToList(); + + for (int i = 0; i < vector.GetNumberOfElements(); i++) + { + int id = _element.Children[i].Id; + vector.SetIndex(newOrder.IndexOf(id), i); + } + } + + return vector; + } + + public string Name => AlgorithmName; + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/ISortAlgorithm.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/ISortAlgorithm.cs new file mode 100644 index 00000000..b8c7a7c4 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/ISortAlgorithm.cs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +namespace DsmSuite.DsmViewer.Application.Sorting +{ + public interface ISortAlgorithm + { + SortResult Sort(); + string Name { get; } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/PartitioningCalculation.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/PartitioningCalculation.cs new file mode 100644 index 00000000..98a42094 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/PartitioningCalculation.cs @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +namespace DsmSuite.DsmViewer.Application.Sorting +{ + /// + /// This class is responsible for the partitioning calculation which for a given square matrix orders the rows + /// as close as possible to the lower block triangular form. The idea being that as many empty cells are pushed + /// into the upper triangle as possible. It is probable that some cells will be non-zero - in this + /// case they are pushed towards the bottom right corner + /// + /// + /// + /// The algorithm first pushes empty rows to the top and complete rows to the bottom. Partially complete rows + /// are then processed using a distance score to push them below but as close as possible to the diagonal + /// The result of the calculation is contained in the vector class which specifies how the new order of row indexes + /// + class PartitioningCalculation + { + readonly WeightsMatrix _sm; + + /// + /// Constructor of calculation on a given n * n matrix + /// + /// + public PartitioningCalculation(WeightsMatrix matrix) + { + _sm = matrix; + } + + /// + /// Run the partition calculation + /// + /// The result in the form of a vector + public SortResult Partition() + { + SortResult vector = new SortResult(_sm.Size); + + DoPartitioning(vector); + + return vector; + } + + /// + /// the main partitioning algorithm. + /// + /// + void DoPartitioning(SortResult vector) + { + // Move all empty rows to the top - an save the index of the first non empty row (start) + int start = MoveZeroRows(vector); + + // Move all the complete rows to the bottom - save the index of the first full row (end) + int end = MoveFullRows(vector, start); + + // For the remaining rows between start and end move to the right empty columns + end = MoveZeroColumns(vector, start, end); + + // Sort the remaining partially complete rows + ToBlockTriangular(vector, start, end); + } + + int MoveZeroRows(SortResult vector) + { + int nextSwapRow = 0; + + // bubble zero rows to the top - starting from the bottom - to leave any rows already moved + // by the user stay in place + + int i = vector.GetNumberOfElements() - 1; + while (i >= nextSwapRow) + { + var allZero = true; + for (int j = 0; j < vector.GetNumberOfElements() && allZero; j++) + { + if (i != j) // the diagonal must obviously be ignored + { + if (TrueMatrixValue(vector, i, j) != 0) + { + allZero = false; + } + } + } + + if (allZero) // swap indexes + { + vector.Swap(nextSwapRow, i); + nextSwapRow++; // points to next swap position + + // don't decrement i as it has not yet been tested - we've changed the order remember !! + } + else + { + i--; // next row + } + } + + return nextSwapRow; + } + + int MoveFullRows(SortResult vector, int start) + { + int nextSwapRow = vector.GetNumberOfElements() - 1; + + // bubble complete rows to the bottom starting 'start' + int i = start; + + while (i <= nextSwapRow) + { + var allNonZero = true; + for (int j = 0; j < vector.GetNumberOfElements() && allNonZero; j++) + { + if (i != j) + { + if (TrueMatrixValue(vector, i, j) == 0) + { + allNonZero = false; + } + } + } + + if (allNonZero) // swap indexes + { + vector.Swap(nextSwapRow, i); + nextSwapRow--; + } + else + { + i++; + } + } + + return nextSwapRow; + } + + int MoveZeroColumns(SortResult vector, int start, int end) + { + int j = start; + int nextSwap = end; //vector.Size - 1; + + while (j <= nextSwap) + { + var allZeros = true; + for (int i = 0; i < vector.GetNumberOfElements() && allZeros; i++) + { + if (i != j) + { + if (TrueMatrixValue(vector, i, j) != 0) + { + allZeros = false; + } + } + } + + if (allZeros) // swap indexes + { + vector.Swap(nextSwap, j); + nextSwap--; + + // stay on new column in position j + } + else + { + j++; + } + } + + return nextSwap; + } + + /// + /// Get the dependency weight from the original square matrix given the current vector and i,j indexes + /// + /// + /// + /// + /// + int TrueMatrixValue(SortResult vector, int i, int j) + { + return _sm.GetWeight(vector.GetIndex(i), vector.GetIndex(j)); + } + + void ToBlockTriangular(SortResult vector, int start, int end) + { + bool doLoop; + + long currentScore = Score(vector); + + // For holding Permutations already examined during one iteration of the outer while loop + IDictionary permMap = + new Dictionary(vector.GetNumberOfElements() * vector.GetNumberOfElements() / 2); + do + { + doLoop = false; + + for (int i = start; i <= end; i++) // i is index on rows + { + for (int j = end; j > i; j--) // cols in upper triangle + { + if (TrueMatrixValue(vector, i, j) != 0) // not zero so we want to possibly move it + { + // now find first zero from the left hand side + + for (int x = start; x <= end; x++) // here x represents the line index + { + for (int y = start; y <= end; y++) + { + if (x != y) // ignore the diagonal + { + if (TrueMatrixValue(vector, x, y) == 0) + { + Permutation p = new Permutation(j, y); + + if (!permMap.ContainsKey(p)) + { + permMap.Add(p, null); + + //check score of potential new vector + vector.Swap(j, y); + + var newScore = Score(vector); + + if (newScore > currentScore) + { + currentScore = newScore; + doLoop = true; // increasing score so continue + } + else + { + vector.Swap(j, y); //swap back to original + } + } + // else permutation already used + } + } + } + } + } + } + } + + permMap.Clear(); + } + while (doLoop); + } + + long Score(SortResult vector) + { + // We're trying to maximize the number of empty cells in the upper triangle + // filled in cells are pushed down - for a set of two orderings the one with the higher + // score should be the most preferable + + + // calculate score for cells in upper triangle (TODO possible optimization between start and end) + long score = 0; + + for (int i = 0; i < vector.GetNumberOfElements() - 1; i++) + { + for (int j = i + 1; j < vector.GetNumberOfElements(); j++) + { + if (TrueMatrixValue(vector, i, j) == 0) + { + score += CellScore(i, j, vector.GetNumberOfElements()); + } + } + } + + return score; + } + + static long CellScore(int i, int j, int size) + { + // a measure of distance from bottom right + int a = (size - i); + int b = j + 1; + + return (a * a * b * b); + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/PartitioningSortAlgorithm.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/PartitioningSortAlgorithm.cs new file mode 100644 index 00000000..cf420f74 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/PartitioningSortAlgorithm.cs @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Model.Interfaces; +using System.Diagnostics; + +namespace DsmSuite.DsmViewer.Application.Sorting +{ + public class PartitionSortAlgorithm : ISortAlgorithm + { + private readonly IDsmModel _model; + private readonly IDsmElement _element; + + public const string AlgorithmName = "Partition"; + + public PartitionSortAlgorithm(object[] args) + { + Debug.Assert(args.Length == 2); + _model = args[0] as IDsmModel; + Debug.Assert(_model != null); + _element = args[1] as IDsmElement; + Debug.Assert(_element != null); + } + + public SortResult Sort() + { + SortResult vector = new SortResult(_element.Children.Count); + if (_element.Children.Count > 1) + { + WeightsMatrix matrix = BuildPartitionMatrix(_element.Children); + + PartitioningCalculation algorithm = new PartitioningCalculation(matrix); + + vector = algorithm.Partition(); + } + + return vector; + } + + public string Name => AlgorithmName; + + private WeightsMatrix BuildPartitionMatrix(IList nodes) + { + WeightsMatrix matrix = new WeightsMatrix(nodes.Count); + + for (int i = 0; i < nodes.Count; i++) + { + IDsmElement provider = nodes[i]; + + for (int j = 0; j < nodes.Count; j++) + { + if (j != i) + { + IDsmElement consumer = nodes[j]; + + int weight = _model.GetDependencyWeight(consumer, provider); + + matrix.SetWeight(i, j, weight > 0 ? 1 : 0); + } + } + } + + return matrix; + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/Permutation.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/Permutation.cs new file mode 100644 index 00000000..7bacc881 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/Permutation.cs @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +namespace DsmSuite.DsmViewer.Application.Sorting +{ + /// + /// Represents a permutation of two values (order not important). + /// + public class Permutation + { + readonly int _first; + readonly int _second; + + /// + /// Constructor + /// + /// + /// + public Permutation(int value1, int value2) + { + // as the order is not important we sort them to make comparing permutations a little easier + if (value1 < value2) + { + _first = value1; + _second = value2; + } + else + { + _first = value2; + _second = value1; + } + } + + /// + /// Equals override so that permutations can be used as a key on a dictionary + /// + /// + /// + public override bool Equals(object obj) + { + Permutation test = obj as Permutation; + if (test != null) + { + return test._first == _first && + test._second == _second; + } + + return false; + } + + /// + /// GetHashCode override so that permutations can be used as a key in a dictionary + /// + /// + public override int GetHashCode() + { + return _first.GetHashCode() ^ _second.GetHashCode(); + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/SortAlgorithmFactory.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/SortAlgorithmFactory.cs new file mode 100644 index 00000000..026cb75c --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/SortAlgorithmFactory.cs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Sorting +{ + public class SortAlgorithmFactory + { + private static readonly Dictionary Algorithms; + + static SortAlgorithmFactory() + { + Algorithms = new Dictionary(); + RegisterAlgorithmTypes(); + } + + public static void RegisterAlgorithm(string name, Type algorithm) + { + Algorithms[name] = algorithm; + } + + public static ISortAlgorithm CreateAlgorithm(IDsmModel model, IDsmElement element, string algorithmName) + { + ISortAlgorithm algorithm = null; + + if (Algorithms.ContainsKey(algorithmName)) + { + Type type = Algorithms[algorithmName]; + object[] args = { model, element }; + object argumentList = args; + algorithm = Activator.CreateInstance(type, argumentList) as ISortAlgorithm; + } + return algorithm; + } + + public static IEnumerable GetSupportedAlgorithms() + { + return Algorithms.Keys; + } + + private static void RegisterAlgorithmTypes() + { + RegisterAlgorithm(PartitionSortAlgorithm.AlgorithmName, typeof(PartitionSortAlgorithm)); + RegisterAlgorithm(AlphabeticalSortAlgorithm.AlgorithmName, typeof(AlphabeticalSortAlgorithm)); + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/SortResult.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/SortResult.cs new file mode 100644 index 00000000..2ab8e4ef --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/SortResult.cs @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Application.Sorting +{ + public class SortResult : ISortResult + { + /// + /// _list contains the ordering as a permutation of indices. If the sorting + /// puts an element on position i that was on position j, then _list[i] == j; + /// + private readonly List _list = new List(); + + public SortResult(List list) + { + _list = new List(list); + } + + public SortResult(int numberOfElements) + { + _list.Clear(); + + for (int i = 0; i < numberOfElements; i++) + { + _list.Add(i); + } + } + + public int GetNumberOfElements() + { + return _list.Count; + } + + public void InvertOrder() + { + List> order = new List>(); + for (int i = 0; i < _list.Count; i++) + { + order.Add(new KeyValuePair(i, _list[i])); + } + + foreach (var v in order) + { + _list[v.Value] = v.Key; + } + } + + public void Swap(int index1, int index2) + { + CheckIndex(index1); + CheckIndex(index2); + int temp = _list[index1]; + _list[index1] = _list[index2]; + _list[index2] = temp; + } + + public void SetIndex(int index, int value) + { + CheckIndex(index); + _list[index] = value; + } + + public int GetIndex(int index) + { + CheckIndex(index); + return _list[index]; + } + + /// + /// Return the sorting order as a permutation of indices. If the sorting + /// puts an element on position i that was on position j, then order[i] == j; + /// + public List GetOrder() + { + return new List(_list); + } + + + public bool IsValid + { + get + { + HashSet set = new HashSet(_list); + + for (int i = 0; i < _list.Count; i++) + { + if (!set.Contains(i)) + return false; + } + return _list.Count > 0; + } + } + + private void CheckIndex(int index) + { + if ((index < 0) || (index >= _list.Count)) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/WeightsMatrix.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/WeightsMatrix.cs new file mode 100644 index 00000000..b128d88f --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Application/Sorting/WeightsMatrix.cs @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +namespace DsmSuite.DsmViewer.Application.Sorting +{ + public class WeightsMatrix : ICloneable + { + readonly int[,] _weights; + readonly int _size; + + public WeightsMatrix(int size) + { + _size = size; + _weights = new int[_size, _size]; + } + + public object Clone() + { + WeightsMatrix sm = new WeightsMatrix(Size); + + for (int i = 0; i < Size; i++) + { + for (int j = 0; j < Size; j++) + { + sm.SetWeight(i, j, GetWeight(i, j)); + } + } + + return sm; + } + + public int Size => _size; + + public void SetWeight(int i, int j, int weight) + { + CheckIndex(i); + CheckIndex(i); + + _weights[i, j] = weight; + } + + public int GetWeight(int i, int j) + { + CheckIndex(i); + CheckIndex(i); + + return _weights[i, j]; + } + + private void CheckIndex(int index) + { + if ((index < 0) || (index >= _size)) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmAction.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmAction.cs new file mode 100644 index 00000000..68b3ab8a --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmAction.cs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Model.Core +{ + public class DsmAction : IDsmAction + { + private List _actions; + + /// + /// Create a new action. For every sub-action, an equivalent DsmAction is created recursively. + /// + public DsmAction(int id, string type, IReadOnlyDictionary data, + IEnumerable actions = null) + { + Id = id; + Type = type; + Data = data; + _actions = actions == null ? null : new List(actions.Select( + a => a is DsmAction ? a : new DsmAction(a.Id, a.Type, a.Data, a.Actions))); + } + + public int Id { get; } + + public string Type { get; } + + public IReadOnlyDictionary Data { get; } + + public IEnumerable Actions { get { return _actions; } } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmActionModel.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmActionModel.cs new file mode 100644 index 00000000..2ea4d685 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmActionModel.cs @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Model.Interfaces; +using DsmSuite.DsmViewer.Model.Persistency; + +namespace DsmSuite.DsmViewer.Model.Core +{ + /// + /// Stores an ordered list of actions.
+ /// actionIds are assigned in increasing order, if not provided by the caller. + ///
+ /// DsmModel forwards calls here. + public class DsmActionModel : IDsmActionModelFileCallback + { + private readonly List _actions; + private int _lastActionId; + + public DsmActionModel() + { + _actions = new List(); + _lastActionId = 0; + } + + public void Clear() + { + _actions.Clear(); + _lastActionId = 0; + } + + /// + /// Create a new action with the given properties and append it to the list. + /// + /// The new action. + public IDsmAction ImportAction(int id, string type, IReadOnlyDictionary data, + IEnumerable actions) + { + if (id > _lastActionId) + { + _lastActionId = id; + } + + DsmAction action = new DsmAction(id, type, data, actions); + _actions.Add(action); + return action; + } + + + /// + /// Create a new action with the given properties, assigning it a sequential number + /// as id. + /// + /// The new action. + public IDsmAction AddAction(string type, IReadOnlyDictionary data, + IEnumerable actions) + { + _lastActionId++; + DsmAction action = new DsmAction(_lastActionId, type, data, actions); + _actions.Add(action); + return action; + } + + + public IEnumerable GetExportedActions() + { + return _actions; + } + + public int GetExportedActionCount() + { + return _actions.Count; + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmDependencies.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmDependencies.cs new file mode 100644 index 00000000..1507b15f --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmDependencies.cs @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Model.Core +{ + public class DsmDependencies + { + private readonly Dictionary _directWeights = new Dictionary(); + private readonly Dictionary _derivedWeights = new Dictionary(); + private readonly Dictionary> _outgoingRelations = new Dictionary>(); + private readonly Dictionary> _ingoingRelations = new Dictionary>(); + private readonly IDsmElement _element; + + public DsmDependencies(IDsmElement element) + { + _element = element; + } + + public void AddIngoingRelation(DsmRelation relation) + { + if (!_ingoingRelations.ContainsKey(relation.Consumer.Id)) + { + _ingoingRelations[relation.Consumer.Id] = new List(); + } + + _ingoingRelations[relation.Consumer.Id].Add(relation); + } + + public void RemoveIngoingRelation(DsmRelation relation) + { + if (_ingoingRelations.ContainsKey(relation.Consumer.Id)) + { + _ingoingRelations[relation.Consumer.Id].Remove(relation); + } + } + + public IEnumerable GetIngoingRelations() + { + List relations = new List(); + foreach (List r in _ingoingRelations.Values) + { + relations.AddRange(r); + } + return relations; + } + + public void AddOutgoingRelation(DsmRelation relation) + { + if (!_outgoingRelations.ContainsKey(relation.Provider.Id)) + { + _outgoingRelations[relation.Provider.Id] = new List(); + } + + _outgoingRelations[relation.Provider.Id].Add(relation); + + AddDirectWeight(relation.Provider, relation.Weight); + } + + public void RemoveOutgoingRelation(DsmRelation relation) + { + if (_outgoingRelations.ContainsKey(relation.Provider.Id)) + { + _outgoingRelations[relation.Provider.Id].Remove(relation); + } + + RemoveDirectWeight(relation.Provider, relation.Weight); + } + + public IEnumerable GetOutgoingRelations() + { + List relations = new List(); + foreach (List r in _outgoingRelations.Values) + { + relations.AddRange(r); + } + return relations; + } + + public IEnumerable GetOutgoingRelations(IDsmElement provider) + { + if (_outgoingRelations.ContainsKey(provider.Id)) + { + return _outgoingRelations[provider.Id]; + } + else + { + return new List(); + } + } + + public void AddDerivedWeight(IDsmElement provider, int weight) + { + int currentWeight; + if (_derivedWeights.TryGetValue(provider.Id, out currentWeight)) + { + _derivedWeights[provider.Id] = currentWeight + weight; + } + else + { + _derivedWeights[provider.Id] = weight; + } + } + + public void RemoveDerivedWeight(IDsmElement provider, int weight) + { + int currentWeight; + if (_derivedWeights.TryGetValue(provider.Id, out currentWeight)) + { + if (currentWeight >= weight) + { + _derivedWeights[provider.Id] = currentWeight - weight; + } + else + { + + } + } + } + + public int GetDerivedDependencyWeight(IDsmElement provider) + { + int weight = 0; + if (_element.Id != provider.Id) + { + _derivedWeights.TryGetValue(provider.Id, out weight); + } + return weight; + } + + public int GetDirectDependencyWeight(IDsmElement provider) + { + int weight = 0; + if (_element.Id != provider.Id) + { + _directWeights.TryGetValue(provider.Id, out weight); + } + return weight; + } + + private void AddDirectWeight(IDsmElement provider, int weight) + { + if (_directWeights.ContainsKey(provider.Id)) + { + _directWeights[provider.Id] += weight; + } + else + { + _directWeights[provider.Id] = weight; + } + } + + private void RemoveDirectWeight(IDsmElement provider, int weight) + { + if (_directWeights.ContainsKey(provider.Id)) + { + _directWeights[provider.Id] -= weight; + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmElement.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmElement.cs new file mode 100644 index 00000000..056fb03d --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmElement.cs @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Model.Core +{ + /// + /// Represent an element in the dsm hierarchy. + /// + public class DsmElement : IDsmElement + { + private char _typeId; + private readonly List _children = new List(); + private DsmElement _parent; + private static readonly NameRegistration ElementTypeNameRegistration = new NameRegistration(); + private static readonly NameRegistration ElementPropertyNameRegistration = new NameRegistration(); + + public DsmElement(int id, string name, string type, IDictionary properties, int order = 0, bool isExpanded = false) + { + Id = id; + Name = name; + _typeId = ElementTypeNameRegistration.RegisterName(type); + Properties = (properties != null) ? properties : new Dictionary(); + Order = order; + IsExpanded = isExpanded; + IsIncludedInTree = true; + Dependencies = new DsmDependencies(this); + + foreach (string key in Properties.Keys) + { + ElementPropertyNameRegistration.RegisterName(key); + } + } + + public DsmDependencies Dependencies { get; } + + public int Id { get; } + + public int Order { get; set; } + + public string Type + { + get { return ElementTypeNameRegistration.GetRegisteredName(_typeId); } + set { _typeId = ElementTypeNameRegistration.RegisterName(value); } + } + + public string Name { get; set; } + + public IDictionary Properties { get; } + + public IEnumerable DiscoveredElementPropertyNames() + { + return ElementPropertyNameRegistration.GetRegisteredNames(); + } + + public bool IsDeleted { get; set; } + + public bool IsBookmarked { get; set; } + + public bool IsRoot => Parent == null; + + public string Fullname + { + get + { + string fullname = Name; + IDsmElement parent = Parent; + while (parent != null) + { + if (parent.Name.Length > 0) + { + fullname = parent.Name + "." + fullname; + } + parent = parent.Parent; + } + return fullname; + } + } + + public string GetRelativeName(IDsmElement element) + { + string fullname = Name; + IDsmElement parent = Parent; + while ((parent != element) && (parent != null)) + { + if (parent.Name.Length > 0) + { + fullname = parent.Name + "." + fullname; + } + parent = parent.Parent; + } + return fullname; + } + + public bool IsExpanded { get; set; } + + public void ExpandRecursively(bool expanded) + { + if (!HasChildren) + return; + + IsExpanded = expanded; + foreach (DsmElement child in Children) + child.ExpandRecursively(expanded); + } + + public bool IsMatch { get; set; } + + public bool IsIncludedInTree { get; set; } + + public IDsmElement Parent => _parent; + + public bool IsRecursiveChildOf(IDsmElement element) + { + bool isRecursiveChildOf = false; + + IDsmElement parent = Parent; + while ((parent != null) && !isRecursiveChildOf) + { + if (parent == element) + { + isRecursiveChildOf = true; + } + + parent = parent.Parent; + } + return isRecursiveChildOf; + } + + public IList Children => _children.Where(child => ((child.IsDeleted == false) && child.IsIncludedInTree)).ToList(); + + public IList AllChildren => _children; + + public int IndexOfChild(IDsmElement child) + { + return _children.IndexOf(child); + } + + public bool ContainsChildWithName(string name) + { + bool containsChildWithName = false; + foreach (IDsmElement child in Children) + { + if (child.Name == name) + { + containsChildWithName = true; + } + } + + return containsChildWithName; + } + + public bool HasChildren => Children.Count > 0; + + public void InsertChildAtEnd(IDsmElement child) + { + _children.Add(child); + DsmElement c = child as DsmElement; + if (c != null) + { + c._parent = this; + } + } + + public void InsertChildAtIndex(IDsmElement child, int index) + { + int rangeLimitedIndex = index; + rangeLimitedIndex = Math.Min(rangeLimitedIndex, _children.Count); + rangeLimitedIndex = Math.Max(rangeLimitedIndex, 0); + _children.Insert(rangeLimitedIndex, child); + DsmElement c = child as DsmElement; + if (c != null) + { + c._parent = this; + } + } + + public void RemoveChild(IDsmElement child) + { + _children.Remove(child); + DsmElement c = child as DsmElement; + if (c != null) + { + c._parent = null; + } + } + + public void RemoveAllChildren() + { + _children.Clear(); + } + + public IDictionary GetElementAndItsChildren() + { + Dictionary elements = new Dictionary(); + GetElementAndItsChildren(this, elements); + return elements; + } + + private void GetElementAndItsChildren(IDsmElement element, Dictionary elements) + { + if (!element.IsDeleted) + { + elements[element.Id] = element as DsmElement; + } + + foreach (IDsmElement child in element.Children) + { + GetElementAndItsChildren(child, elements); + } + } + + public bool Swap(IDsmElement element1, IDsmElement element2) + { + bool swapped = false; + + if (_children.Contains(element1) && _children.Contains(element2)) + { + int index1 = _children.IndexOf(element1); + int index2 = _children.IndexOf(element2); + + _children[index2] = element1; + _children[index1] = element2; + + swapped = true; + } + + return swapped; + } + + public int CompareTo(object obj) + { + DsmElement element = obj as DsmElement; + return Id.CompareTo(element?.Id); + } + + public static IEnumerable GetTypeNames() + { + return ElementTypeNameRegistration.GetRegisteredNames(); + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmElementModel.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmElementModel.cs new file mode 100644 index 00000000..af676179 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmElementModel.cs @@ -0,0 +1,535 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.Common.Util; +using DsmSuite.DsmViewer.Model.Interfaces; +using DsmSuite.DsmViewer.Model.Persistency; + +namespace DsmSuite.DsmViewer.Model.Core +{ + /// + /// Manages the elements of a model. Only used by DsmModel, which forwards calls here. + /// + public class DsmElementModel : IDsmElementModelFileCallback + { + private readonly Dictionary _elementsById; + private readonly Dictionary _elementsByName; + private readonly Dictionary _deletedElementsById; + private int _lastElementId; + private readonly DsmElement _root; + private readonly DsmRelationModel _relationModel; + + public DsmElementModel(DsmRelationModel relationModel) + { + _relationModel = relationModel; + _elementsById = new Dictionary(); + _elementsByName = new Dictionary(); + _deletedElementsById = new Dictionary(); + _root = new DsmElement(0, "", "", null); + Clear(); + } + + public void Clear() + { + _elementsById.Clear(); + _deletedElementsById.Clear(); + // Changed 2026-07 for CSharpCodeAnalyst: _elementsByName was not cleared here. AddElement + // resolves through FindElementByFullname, so after a Clear it returned stale elements from + // the previous population, which are no longer registered by id nor attached to the root. + // Upstream never hit this because every import runs against a freshly constructed model. + _elementsByName.Clear(); + _root.RemoveAllChildren(); + _lastElementId = 0; + RegisterElement(_root); + } + + public void ClearHistory() + { + _deletedElementsById.Clear(); + } + + public IDsmElement ImportElement(int id, string name, string type, IDictionary properties, int order, bool expanded, int? parentId, bool deleted) + { + Logger.LogDataModelMessage($"Import element id={id} name={name} type={type} order={order} expanded={expanded} parentId={parentId}"); + + if (id > _lastElementId) + { + _lastElementId = id; + } + return AddElement(id, name, type, null, properties, order, expanded, parentId, deleted); + } + + public IDsmElement AddElement(string name, string type, int? parentId, int? index, IDictionary properties) + { + Logger.LogDataModelMessage($"Add element name={name} type={type} parentId={parentId}"); + + string fullname = name; + if (parentId.HasValue) + { + if (_elementsById.ContainsKey(parentId.Value)) + { + ElementName elementName = new ElementName(_elementsById[parentId.Value].Fullname); + elementName.AddNamePart(name); + fullname = elementName.FullName; + } + } + + IDsmElement element = FindElementByFullname(fullname); + if (element == null) + { + _lastElementId++; + element = AddElement(_lastElementId, name, type, index, properties, 0, false, parentId, false); + } + + return element; + } + + public void ChangeElementName(IDsmElement element, string name) + { + DsmElement changedElement = element as DsmElement; + if (changedElement != null) + { + UnregisterElementNameHierarchy(changedElement); + changedElement.Name = name; + RegisterElementNameHierarchy(changedElement); + } + } + + public void ChangeElementType(IDsmElement element, string type) + { + DsmElement changedElement = element as DsmElement; + if (changedElement != null) + { + changedElement.Type = type; + } + } + + public bool IsChangeElementParentAllowed(IDsmElement element, IDsmElement parent) + { + DsmElement changedElement = element as DsmElement; + DsmElement currentParent = element.Parent as DsmElement; + DsmElement newParent = parent as DsmElement; + return ((currentParent != null) && + (newParent != null) && + (currentParent != newParent) && // Do not allow new parent same as current parent + !newParent.IsRecursiveChildOf(changedElement)); // Do not allow new parent being a child of the changed element + } + + public void ChangeElementParent(IDsmElement element, IDsmElement parent, int index) + { + Logger.LogDataModelMessage($"Change element parent name={element.Name} from {element.Parent.Fullname} to {parent.Fullname}"); + + if (IsChangeElementParentAllowed(element, parent)) + { + DsmElement changedElement = element as DsmElement; + DsmElement currentParent = element.Parent as DsmElement; + DsmElement newParent = parent as DsmElement; + + if ((changedElement != null) && (currentParent != null) & (newParent != null)) + { + IEnumerable externalRelations = _relationModel.FindExternalRelations(element).ToList(); + + foreach (IDsmRelation relation in externalRelations) + { + _relationModel.RemoveWeights(relation); + } + + UnregisterElementNameHierarchy(changedElement); + currentParent.RemoveChild(element); + CollapseIfNoChildrenLeft(currentParent); + + newParent.InsertChildAtIndex(element, index); + RegisterElementNameHierarchy(changedElement); + + foreach (IDsmRelation relation in externalRelations) + { + _relationModel.AddWeights(relation); + } + } + } + } + + public void RemoveElement(int elementId) + { + Logger.LogDataModelMessage($"Remove element id={elementId}"); + + if (_elementsById.ContainsKey(elementId)) + { + DsmElement element = _elementsById[elementId]; + UnregisterElement(element); + + CollapseIfNoChildrenLeft(element.Parent); + } + } + + public void UnremoveElement(int elementId) + { + Logger.LogDataModelMessage($"Restore element id={elementId}"); + + if (_deletedElementsById.ContainsKey(elementId)) + { + DsmElement element = _deletedElementsById[elementId]; + ReregisterElement(element); + } + } + + public IEnumerable GetElementTypes() + { + return DsmElement.GetTypeNames(); + } + + public IEnumerable GetElements() + { + return _elementsById.Values; + } + + public int GetElementCount() + { + return _elementsById.Count; + } + + public IDsmElement RootElement + { + get + { + return _root; + } + } + + public int GetExportedElementCount() + { + return _elementsById.Count + _deletedElementsById.Count - 1; // Root not written/read + } + + public void AssignElementOrder() + { + Logger.LogDataModelMessage("AssignElementOrder"); + + int order = 1; + foreach (IDsmElement root in _root.Children) + { + DsmElement rootElement = root as DsmElement; + if (rootElement != null) + { + AssignElementOrder(rootElement, ref order); + } + } + } + + public IDsmElement FindElementById(int elementId) + { + return _elementsById.ContainsKey(elementId) ? _elementsById[elementId] : null; + } + + public IDsmElement FindElementByFullname(string fullname) + { + return _elementsByName.ContainsKey(fullname) ? _elementsByName[fullname] : null; + } + + public IList SearchElements(string searchText, IDsmElement searchInElement, bool caseSensitive, string elementTypeFilter, bool markMatchingElements) + { + List matchingElements = new List(); + if (searchText != null) + { + string fullname = ""; + string text = caseSensitive ? searchText : searchText.ToLower(); + + if (text.Length > 0) + { + RecursiveSearchElements(searchInElement, text, caseSensitive, elementTypeFilter, markMatchingElements, fullname, matchingElements); + } + else + { + ClearMarkElements(_root); + } + } + return matchingElements; + } + + private bool RecursiveSearchElements(IDsmElement element, string searchText, bool caseSensitive, string elementTypeFilter, bool markMatchingElements, string fullname, IList matchingElements) + { + bool isMatch = false; + + if (fullname.Length > 0) + { + fullname += "."; + } + if (caseSensitive) + { + fullname += element.Name; + } + else + { + fullname += element.Name.ToLower(); + } + + if (fullname.Contains(searchText) && IsElementFilterMatch(element, elementTypeFilter) && !element.IsDeleted) + { + // Add to list and mark element as search match + isMatch = true; + matchingElements.Add(element); + } + + foreach (IDsmElement child in element.Children) + { + if (RecursiveSearchElements(child, searchText, caseSensitive, elementTypeFilter, markMatchingElements, fullname, matchingElements)) + { + // Add parent element as match when it contains a matching child + isMatch = true; + } + } + + if (markMatchingElements) + { + element.IsMatch = isMatch; + } + + return isMatch; + } + + private bool IsElementFilterMatch(IDsmElement element, string elementTypeFilter) + { + return string.IsNullOrEmpty(elementTypeFilter) || (elementTypeFilter == element.Type); + } + + private void ClearMarkElements(IDsmElement element) + { + element.IsMatch = false; + + foreach (IDsmElement child in element.Children) + { + ClearMarkElements(child); + } + } + public IDsmElement GetDeletedElementById(int id) + { + return _deletedElementsById.ContainsKey(id) ? _deletedElementsById[id] : null; + } + + public void ReorderChildren(IDsmElement element, ISortResult sortResult) + { + DsmElement parent = element as DsmElement; + if (parent != null) + { + List clonedChildren = new List(parent.Children); + + foreach (IDsmElement child in clonedChildren) + { + parent.RemoveChild(child); + } + + for (int i = 0; i < sortResult.GetNumberOfElements(); i++) + { + parent.InsertChildAtEnd(clonedChildren[sortResult.GetIndex(i)]); + } + } + } + + public bool Swap(IDsmElement element1, IDsmElement element2) + { + bool swapped = false; + + if (element1?.Parent == element2?.Parent) + { + DsmElement parent = element1?.Parent as DsmElement; + if (parent != null) + { + swapped = parent.Swap(element1, element2); + } + } + + return swapped; + } + + public IDsmElement NextSibling(IDsmElement element) + { + IDsmElement nextSibling = null; + DsmElement parent = element?.Parent as DsmElement; + if (parent != null) + { + int index = parent.Children.IndexOf(element); + + if (index < parent.Children.Count - 1) + { + nextSibling = parent.Children[index + 1]; + } + } + return nextSibling; + } + + public IDsmElement PreviousSibling(IDsmElement element) + { + IDsmElement previousSibling = null; + DsmElement parent = element?.Parent as DsmElement; + if (parent != null) + { + int index = parent.Children.IndexOf(element); + + if (index > 0) + { + previousSibling = parent.Children[index - 1]; + } + } + return previousSibling; + } + + public void UpdateChildrenIncludeInTree(IDsmElement element, bool included) + { + element.IsIncludedInTree = included; + + foreach (IDsmElement child in element.AllChildren) + { + UpdateChildrenIncludeInTree(child, included); + } + } + + public void UpdateParentsIncludeInTree(IDsmElement element, bool included) + { + IDsmElement current = element; + do + { + current.IsIncludedInTree = included; + current = current.Parent; + } while (current != null); + } + + + public IDsmElement AddElement(int id, string name, string type, IDictionary properties, int order) + { + DsmElement element = new DsmElement(id, name, type, properties) { Order = order, IsExpanded = false, IsDeleted = false }; + RegisterElement(element); + return element; + } + + private IDsmElement AddElement(int id, string name, string type, int? index, IDictionary properties, int order, bool expanded, int? parentId, bool deleted) + { + DsmElement element = new DsmElement(id, name, type, properties) { Order = order, IsExpanded = expanded, IsDeleted = deleted }; + + if (parentId.HasValue) + { + DsmElement parent = null; + if (_elementsById.ContainsKey(parentId.Value)) + { + parent = _elementsById[parentId.Value]; + } + + if (_deletedElementsById.ContainsKey(parentId.Value)) + { + parent = _deletedElementsById[parentId.Value]; + } + + if (parent != null) + { + if (index.HasValue) + { + parent.InsertChildAtIndex(element, index.Value); + } + else + { + parent.InsertChildAtEnd(element); + } + } + else + { + Logger.LogError($"Parent not found id={id}"); + } + } + else + { + _root.InsertChildAtEnd(element); + _root.IsExpanded = true; + } + + if (deleted) + { + UnregisterElement(element); + } + else + { + RegisterElement(element); + } + + return element; + } + + private void AssignElementOrder(DsmElement element, ref int order) + { + element.Order = order; + order++; + + foreach (IDsmElement child in element.Children) + { + DsmElement childElement = child as DsmElement; + if (childElement != null) + { + AssignElementOrder(childElement, ref order); + } + } + } + + private void CollapseIfNoChildrenLeft(IDsmElement element) + { + DsmElement e = element as DsmElement; + if (e != null) + { + if (element.Children.Count == 0) + { + e.IsExpanded = false; + } + } + } + + private void RegisterElement(DsmElement element) + { + _elementsById[element.Id] = element; + _elementsByName[element.Fullname] = element; + } + + private void UnregisterElement(DsmElement element) + { + _relationModel.UnregisterElementRelations(element); + + element.IsDeleted = true; + _deletedElementsById[element.Id] = element; + _elementsById.Remove(element.Id); + _elementsByName.Remove(element.Fullname); + + foreach (IDsmElement child in element.AllChildren) + { + UnregisterElement(child as DsmElement); + } + } + + private void ReregisterElement(DsmElement element) + { + foreach (IDsmElement child in element.AllChildren) + { + ReregisterElement(child as DsmElement); + } + + _elementsById[element.Id] = element; + _deletedElementsById.Remove(element.Id); + element.IsDeleted = false; + + _relationModel.ReregisterElementRelations(element); + } + + private void UnregisterElementNameHierarchy(DsmElement element) + { + _elementsByName.Remove(element.Fullname); + + foreach (DsmElement child in element.AllChildren) + { + UnregisterElementNameHierarchy(child); + } + } + + private void RegisterElementNameHierarchy(DsmElement element) + { + _elementsByName[element.Fullname] = element; + + foreach (DsmElement child in element.AllChildren) + { + RegisterElementNameHierarchy(child); + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmModel.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmModel.cs new file mode 100644 index 00000000..8197edfc --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmModel.cs @@ -0,0 +1,347 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.Common.Model.Core; +using DsmSuite.Common.Model.Interface; +using DsmSuite.Common.Util; +using DsmSuite.DsmViewer.Model.Interfaces; +using DsmSuite.DsmViewer.Model.Persistency; +using System.Reflection; + +namespace DsmSuite.DsmViewer.Model.Core +{ + public class DsmModel : IDsmModel + { + private readonly MetaDataModel _metaDataModel; + private readonly DsmElementModel _elementsDataModel; + private readonly DsmRelationModel _relationsDataModel; + private readonly DsmActionModel _actionsDataModel; + + public DsmModel(string processStep, Assembly executingAssembly) + { + _metaDataModel = new MetaDataModel(processStep, executingAssembly); + + _relationsDataModel = new DsmRelationModel(); + _elementsDataModel = new DsmElementModel(_relationsDataModel); + _actionsDataModel = new DsmActionModel(); + } + + public void LoadModel(string dsmFilename, IProgress progress) + { + Logger.LogDataModelMessage($"Load data model file={dsmFilename}"); + + Clear(); + DsmModelFile dsmModelFile = new DsmModelFile(dsmFilename, _metaDataModel, _elementsDataModel, _relationsDataModel, _actionsDataModel); + dsmModelFile.Load(progress); + IsCompressed = dsmModelFile.IsCompressedFile(); + ModelFilename = dsmFilename; + } + + public void SaveModel(string dsmFilename, bool compressFile, IProgress progress) + { + Logger.LogDataModelMessage($"Save data model file={dsmFilename} compress={compressFile}"); + + _metaDataModel.AddMetaDataItemToDefaultGroup("Total elements found", $"{GetExportedElementCount()}"); + + DsmModelFile dsmModelFile = new DsmModelFile(dsmFilename, _metaDataModel, _elementsDataModel, _relationsDataModel, _actionsDataModel); + dsmModelFile.Save(compressFile, progress); + ModelFilename = dsmFilename; + } + + public string ModelFilename { get; private set; } + + public bool IsCompressed { get; private set; } + + public void Clear() + { + _metaDataModel.Clear(); + _elementsDataModel.Clear(); + _relationsDataModel.Clear(); + _actionsDataModel.Clear(); + } + + public IMetaDataItem AddMetaData(string name, string value) + { + return _metaDataModel.AddMetaDataItemToDefaultGroup(name, value); + } + + public IMetaDataItem AddMetaData(string group, string name, string value) + { + return _metaDataModel.AddMetaDataItem(group, name, value); + } + + public IEnumerable GetMetaDataGroups() + { + return _metaDataModel.GetExportedMetaDataGroups(); + } + + public IEnumerable GetMetaDataGroupItems(string groupName) + { + return _metaDataModel.GetExportedMetaDataGroupItems(groupName); + } + + public IDsmAction AddAction(string type, IReadOnlyDictionary data, + IEnumerable actions) + { + return _actionsDataModel.AddAction(type, data, actions); + } + + public void ClearActions() + { + _actionsDataModel.Clear(); + } + + public IEnumerable GetActions() + { + return _actionsDataModel.GetExportedActions(); + } + + public IDsmElement AddElement(int id, string name, string type, IDictionary properties, int order) + { + return _elementsDataModel.AddElement(id, name, type, properties, order); + } + + public IDsmElement AddElement(string name, string type, int? parentId, int? index, IDictionary properties) + { + return _elementsDataModel.AddElement(name, type, parentId, index, properties); + } + + public void ChangeElementName(IDsmElement element, string name) + { + _elementsDataModel.ChangeElementName(element, name); + } + + public void ChangeElementType(IDsmElement element, string type) + { + _elementsDataModel.ChangeElementType(element, type); + } + + public bool IsChangeElementParentAllowed(IDsmElement element, IDsmElement parent) + { + return _elementsDataModel.IsChangeElementParentAllowed(element, parent); + } + + public void ChangeElementParent(IDsmElement element, IDsmElement parent, int index) + { + _elementsDataModel.ChangeElementParent(element, parent, index); + } + + public void RemoveElement(int elementId) + { + _elementsDataModel.RemoveElement(elementId); + } + + public void UnremoveElement(int elementId) + { + _elementsDataModel.UnremoveElement(elementId); + } + + public void IncludeInTree(IDsmElement element, bool included) + { + UpdateChildrenIncludeInTree(element, included); + UpdateParentsIncludeInTree(element, included); + } + + private void UpdateChildrenIncludeInTree(IDsmElement element, bool included) + { + _elementsDataModel.UpdateChildrenIncludeInTree(element, included); + } + + private void UpdateParentsIncludeInTree(IDsmElement element, bool included) + { + _elementsDataModel.UpdateParentsIncludeInTree(element, included); + } + + public IEnumerable GetElementTypes() + { + return _elementsDataModel.GetElementTypes(); + } + + public IEnumerable GetElements() + { + return _elementsDataModel.GetElements(); + } + + public int GetElementCount() + { + return _elementsDataModel.GetElementCount(); + } + + public IDsmElement RootElement + { + get + { + return _elementsDataModel.RootElement; + } + } + + public int GetExportedElementCount() + { + return _elementsDataModel.GetExportedElementCount(); + } + + public void AssignElementOrder() + { + _elementsDataModel.AssignElementOrder(); + } + + public IDsmElement GetElementById(int id) + { + return _elementsDataModel.FindElementById(id); + } + + public IDsmElement GetElementByFullname(string fullname) + { + return _elementsDataModel.FindElementByFullname(fullname); + } + + public IList SearchElements(string searchText, IDsmElement searchInElement, bool caseSensitive, string elementTypeFilter, bool markMatchingElements) + { + return _elementsDataModel.SearchElements(searchText, searchInElement, caseSensitive, elementTypeFilter, markMatchingElements); + } + + public IDsmElement GetDeletedElementById(int id) + { + return _elementsDataModel.GetDeletedElementById(id); + } + + public IDsmRelation AddRelation(IDsmElement consumer, IDsmElement provider, string type, int weight, IDictionary properties) + { + return _relationsDataModel.AddRelation(consumer, provider, type, weight, properties); + } + + public IDsmRelation AddRelation(int id, IDsmElement consumer, IDsmElement provider, string type, int weight, IDictionary properties) + { + return _relationsDataModel.AddRelation(id, consumer, provider, type, weight, properties); + } + + + public void ChangeRelationType(IDsmRelation relation, string type) + { + _relationsDataModel.ChangeRelationType(relation, type); + } + + public void ChangeRelationWeight(IDsmRelation relation, int weight) + { + _relationsDataModel.ChangeRelationWeight(relation, weight); + } + + public void RemoveRelation(int relationId) + { + _relationsDataModel.RemoveRelation(relationId); + } + + public void UnremoveRelation(int relationId) + { + _relationsDataModel.UnremoveRelation(relationId); + } + + public IEnumerable GetRelationTypes() + { + return _relationsDataModel.GetRelationTypes(); + } + + public int GetDependencyWeight(IDsmElement consumer, IDsmElement provider) + { + return _relationsDataModel.GetDependencyWeight(consumer, provider); + } + + public int GetDirectDependencyWeight(IDsmElement consumer, IDsmElement provider) + { + return _relationsDataModel.GetDirectDependencyWeight(consumer, provider); + } + + public CycleType IsCyclicDependency(IDsmElement consumer, IDsmElement provider) + { + return _relationsDataModel.IsCyclicDependency(consumer, provider); + } + + public IDsmRelation GetRelationById(int relationId) + { + return _relationsDataModel.GetRelationById(relationId); + } + + public IDsmRelation GetDeletedRelationById(int relationId) + { + return _relationsDataModel.GetDeletedRelationById(relationId); + } + + public IEnumerable GetRelations() + { + return _relationsDataModel.GetRelations(); + } + + public int GetRelationCount() + { + return _relationsDataModel.GetRelationCount(); + } + + public IEnumerable FindRelations(IDsmElement consumer, IDsmElement provider) + { + return _relationsDataModel.FindRelations(consumer, provider); + } + + public int GetRelationCount(IDsmElement consumer, IDsmElement provider) + { + return _relationsDataModel.GetRelationCount(consumer, provider); + } + + public IDsmRelation FindRelation(IDsmElement consumer, IDsmElement provider, string type) + { + return _relationsDataModel.FindRelation(consumer, provider, type); + } + + public IEnumerable FindIngoingRelations(IDsmElement element) + { + return _relationsDataModel.FindIngoingRelations(element); + } + + public IEnumerable FindOutgoingRelations(IDsmElement element) + { + return _relationsDataModel.FindOutgoingRelations(element); + } + + public IEnumerable FindInternalRelations(IDsmElement element) + { + return _relationsDataModel.FindInternalRelations(element); + } + + public IEnumerable FindExternalRelations(IDsmElement element) + { + return _relationsDataModel.FindExternalRelations(element); + } + + public int GetHierarchicalCycleCount(IDsmElement element) + { + return _relationsDataModel.GetHierarchicalCycleCount(element); + } + + public int GetSystemCycleCount(IDsmElement element) + { + return _relationsDataModel.GetSystemCycleCount(element); + } + + public void ReorderChildren(IDsmElement element, ISortResult sortResult) + { + _elementsDataModel.ReorderChildren(element, sortResult); + } + + public bool Swap(IDsmElement element1, IDsmElement element2) + { + return _elementsDataModel.Swap(element1, element2); + } + + public IDsmElement NextSibling(IDsmElement element) + { + return _elementsDataModel.NextSibling(element); + } + + public IDsmElement PreviousSibling(IDsmElement element) + { + return _elementsDataModel.PreviousSibling(element); + } + + public int GetActionCount() + { + return _actionsDataModel.GetExportedActionCount(); + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmRelation.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmRelation.cs new file mode 100644 index 00000000..b4574c59 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmRelation.cs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Model.Core +{ + /// + /// Relation between two elements. + /// + public class DsmRelation : IDsmRelation + { + private char _typeId; + private static readonly NameRegistration RelationTypeNameRegistration = new NameRegistration(); + private static readonly NameRegistration RelationPropertyNameRegistration = new NameRegistration(); + + public DsmRelation(int id, IDsmElement consumer, IDsmElement provider, string type, int weight, IDictionary properties) + { + Id = id; + Consumer = consumer; + Provider = provider; + _typeId = RelationTypeNameRegistration.RegisterName(type); + Weight = weight; + Properties = (properties != null) ? properties : new Dictionary(); + + foreach (string key in Properties.Keys) + { + RelationPropertyNameRegistration.RegisterName(key); + } + } + + public int Id { get; } + + public IDsmElement Consumer { get; } + + public IDsmElement Provider { get; } + + public string Type + { + get { return RelationTypeNameRegistration.GetRegisteredName(_typeId); } + set { _typeId = RelationTypeNameRegistration.RegisterName(value); } + } + + public int Weight { get; set; } + + public IDictionary Properties { get; } + + public IEnumerable DiscoveredRelationPropertyNames() + { + return RelationPropertyNameRegistration.GetRegisteredNames(); + } + + public bool IsDeleted { get; set; } + + public static IEnumerable GetTypeNames() + { + return RelationTypeNameRegistration.GetRegisteredNames(); + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmRelationModel.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmRelationModel.cs new file mode 100644 index 00000000..60710aef --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/DsmRelationModel.cs @@ -0,0 +1,528 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.Common.Util; +using DsmSuite.DsmViewer.Model.Interfaces; +using DsmSuite.DsmViewer.Model.Persistency; + +namespace DsmSuite.DsmViewer.Model.Core +{ + /// + /// Manages the relations of a model. Only used by DsmModel, which forwards calls here. + /// + public class DsmRelationModel : IDsmRelationModelFileCallback + { + private readonly Dictionary _relationsById; + private readonly Dictionary _deletedRelationsById; + + private int _lastRelationId; + + public DsmRelationModel() + { + _relationsById = new Dictionary(); + _deletedRelationsById = new Dictionary(); + _lastRelationId = 0; + } + + public void Clear() + { + _relationsById.Clear(); + _deletedRelationsById.Clear(); + + _lastRelationId = 0; + } + + public void ClearHistory() + { + _deletedRelationsById.Clear(); + } + + public IDsmRelation ImportRelation(int relationId, IDsmElement consumer, IDsmElement provider, string type, int weight, IDictionary properties, bool deleted) + { + DsmRelation relation = null; + + if ((consumer != null) && (provider != null)) + { + Logger.LogDataModelMessage( + $"Import relation relationId={relationId} consumerId={consumer.Id} providerId={provider.Id} type={type} weight={weight}"); + + if (relationId > _lastRelationId) + { + _lastRelationId = relationId; + } + + + if (consumer.Id != provider.Id) + { + relation = new DsmRelation(relationId, consumer, provider, type, weight, properties) { IsDeleted = deleted }; + if (deleted) + { + UnregisterRelation(relation); + } + else + { + RegisterRelation(relation); + } + } + } + + return relation; + } + + public IDsmRelation AddRelation(IDsmElement consumer, IDsmElement provider, string type, int weight, IDictionary properties) + { + DsmRelation relation = null; + + if ((consumer != null) && (provider != null)) + { + Logger.LogDataModelMessage( + $"Add relation consumerId={consumer.Id} providerId={provider.Id} type={type} weight={weight}"); + + if (consumer.Id != provider.Id) + { + _lastRelationId++; + relation = new DsmRelation(_lastRelationId, consumer, provider, type, weight, properties) { IsDeleted = false }; + RegisterRelation(relation); + } + } + + return relation; + } + + public IDsmRelation AddRelation(int id, IDsmElement consumer, IDsmElement provider, string type, int weight, IDictionary properties) + { + DsmRelation relation = null; + + if ((consumer != null) && (provider != null)) + { + Logger.LogDataModelMessage( + $"Add relation consumerId={consumer.Id} providerId={provider.Id} type={type} weight={weight}"); + + if (consumer.Id != provider.Id) + { + relation = new DsmRelation(id, consumer, provider, type, weight, properties) { IsDeleted = false }; + RegisterRelation(relation); + } + } + + return relation; + } + + public void ChangeRelationType(IDsmRelation relation, string type) + { + DsmRelation changedRelation = relation as DsmRelation; + if (changedRelation != null) + { + UnregisterRelation(changedRelation); + + changedRelation.Type = type; + + RegisterRelation(changedRelation); + } + } + + public IEnumerable GetRelationTypes() + { + return DsmRelation.GetTypeNames(); + } + + public void ChangeRelationWeight(IDsmRelation relation, int weight) + { + DsmRelation changedRelation = relation as DsmRelation; + if (changedRelation != null) + { + UnregisterRelation(changedRelation); + + changedRelation.Weight = weight; + + RegisterRelation(changedRelation); + } + } + + public void RemoveRelation(int relationId) + { + if (_relationsById.ContainsKey(relationId)) + { + DsmRelation relation = _relationsById[relationId]; + if (relation != null) + { + UnregisterRelation(relation); + } + } + } + + public void UnremoveRelation(int relationId) + { + if (_deletedRelationsById.ContainsKey(relationId)) + { + DsmRelation relation = _deletedRelationsById[relationId]; + if (relation != null) + { + RegisterRelation(relation); + } + } + } + + public int GetDependencyWeight(IDsmElement consumer, IDsmElement provider) + { + int weight = 0; + DsmElement consumerElement = consumer as DsmElement; + if (consumerElement != null) + { + weight = consumerElement.Dependencies.GetDerivedDependencyWeight(provider); + } + return weight; + } + + public int GetDirectDependencyWeight(IDsmElement consumer, IDsmElement provider) + { + int weight = 0; + DsmElement consumerElement = consumer as DsmElement; + if (consumerElement != null) + { + weight = consumerElement.Dependencies.GetDirectDependencyWeight(provider); + } + return weight; + } + + public CycleType IsCyclicDependency(IDsmElement consumer, IDsmElement provider) + { + if ((GetDirectDependencyWeight(consumer, provider) > 0) && + (GetDirectDependencyWeight(provider, consumer) > 0)) + { + return CycleType.System; + } + else if ((GetDependencyWeight(consumer, provider) > 0) && + (GetDependencyWeight(provider, consumer) > 0)) + { + return CycleType.Hierarchical; + } + else + { + return CycleType.None; + } + } + + public DsmRelation GetRelationById(int id) + { + return _relationsById.ContainsKey(id) ? _relationsById[id] : null; + } + + public DsmRelation GetDeletedRelationById(int id) + { + return _deletedRelationsById.ContainsKey(id) ? _deletedRelationsById[id] : null; + } + + public IDsmRelation FindRelation(IDsmElement consumer, IDsmElement provider, string type) + { + DsmRelation foundRelation = null; + foreach (DsmRelation relation in FindRelations(consumer, provider)) + { + if (relation.Type == type) + { + foundRelation = relation; + } + } + return foundRelation; + } + + public IEnumerable FindRelations(IDsmElement consumer, IDsmElement provider) + { + IList relations = new List(); + + DsmElement consumerDsmElement = consumer as DsmElement; + DsmElement providerDsmElement = provider as DsmElement; + if (consumerDsmElement?.Dependencies != null && providerDsmElement?.Dependencies != null) + { + IDictionary ps = providerDsmElement.GetElementAndItsChildren(); + IDictionary cs = consumerDsmElement.GetElementAndItsChildren(); + + foreach (DsmElement c in cs.Values) + { + foreach (DsmElement p in ps.Values) + { + foreach (DsmRelation relation in c.Dependencies.GetOutgoingRelations(p)) + { + if (!relation.IsDeleted) + { + relations.Add(relation); + } + } + } + } + } + return relations; + } + + public int GetRelationCount(IDsmElement consumer, IDsmElement provider) + { + int count = 0; + + DsmElement consumerDsmElement = consumer as DsmElement; + DsmElement providerDsmElement = provider as DsmElement; + if (consumerDsmElement?.Dependencies != null && providerDsmElement?.Dependencies != null) + { + IDictionary ps = providerDsmElement.GetElementAndItsChildren(); + IDictionary cs = consumerDsmElement.GetElementAndItsChildren(); + + foreach (DsmElement c in cs.Values) + { + foreach (DsmElement p in ps.Values) + { + foreach (DsmRelation relation in c.Dependencies.GetOutgoingRelations(p)) + { + if (!relation.IsDeleted) + { + count++; + } + } + } + } + } + return count; + } + + public IEnumerable FindIngoingRelations(IDsmElement element) + { + return FindRelations(element, RelationDirection.Ingoing, RelationScope.External); + } + + public IEnumerable FindOutgoingRelations(IDsmElement element) + { + return FindRelations(element, RelationDirection.Outgoing, RelationScope.External); + } + + public IEnumerable FindInternalRelations(IDsmElement element) + { + return FindRelations(element, RelationDirection.Outgoing, RelationScope.Internal); + } + + public IEnumerable FindExternalRelations(IDsmElement element) + { + return FindRelations(element, RelationDirection.Both, RelationScope.External); + } + + private IEnumerable FindRelations(IDsmElement element, RelationDirection direction, RelationScope scope) + { + List relations = new List(); + + DsmElement dsmElement = element as DsmElement; + if (dsmElement?.Dependencies != null) + { + IDictionary elements = dsmElement.GetElementAndItsChildren(); + + foreach (DsmElement e in elements.Values) + { + if (direction != RelationDirection.Ingoing) + { + foreach (DsmRelation relation in e.Dependencies.GetOutgoingRelations()) + { + if (HasSelectedScope(scope, relation.Provider, elements) && !relation.IsDeleted) + { + relations.Add(relation); + } + } + } + + if (direction != RelationDirection.Outgoing) + { + foreach (DsmRelation relation in e.Dependencies.GetIngoingRelations()) + { + if (HasSelectedScope(scope, relation.Consumer, elements) && !relation.IsDeleted) + { + relations.Add(relation); + } + } + } + } + } + return relations; + } + + private bool HasSelectedScope(RelationScope scope, IDsmElement element, IDictionary elements) + { + switch (scope) + { + case RelationScope.Internal: + return elements.ContainsKey(element.Id); + case RelationScope.External: + return !elements.ContainsKey(element.Id); + case RelationScope.Both: + return true; + default: + return true; + } + } + + public int GetHierarchicalCycleCount(IDsmElement element) + { + int cyclesCount = 0; + CountCycles(element, CycleType.Hierarchical, ref cyclesCount); + return cyclesCount / 2; + } + + public int GetSystemCycleCount(IDsmElement element) + { + int cyclesCount = 0; + CountCycles(element, CycleType.System, ref cyclesCount); + return cyclesCount / 2; + } + + public IEnumerable GetRelations() + { + return _relationsById.Values; + } + + public int GetRelationCount() + { + return _relationsById.Values.Count; + } + + public IEnumerable GetExportedRelations() + { + List exportedRelations = new List(); + exportedRelations.AddRange(_relationsById.Values); + exportedRelations.AddRange(_deletedRelationsById.Values); + return exportedRelations.OrderBy(x => x.Id); + } + + public int GetExportedRelationCount() + { + return _relationsById.Values.Count + _deletedRelationsById.Values.Count; + } + + public void UnregisterElementRelations(IDsmElement element) + { + List toBeRelationsUnregistered = new List(); + + foreach (DsmRelation relation in _relationsById.Values) + { + if ((element.Id == relation.Consumer.Id) || + (element.Id == relation.Provider.Id)) + { + toBeRelationsUnregistered.Add(relation); + } + } + + foreach (DsmRelation relation in toBeRelationsUnregistered) + { + UnregisterRelation(relation); + } + } + + public void ReregisterElementRelations(IDsmElement element) + { + List toBeRelationsReregistered = new List(); + + foreach (DsmRelation relation in _deletedRelationsById.Values) + { + if ((element.Id == relation.Consumer.Id) || + (element.Id == relation.Provider.Id)) + { + toBeRelationsReregistered.Add(relation); + } + } + + foreach (DsmRelation relation in toBeRelationsReregistered) + { + RegisterRelation(relation); + } + } + + private void RegisterRelation(DsmRelation relation) + { + relation.IsDeleted = false; + _relationsById[relation.Id] = relation; + + if (_deletedRelationsById.ContainsKey(relation.Id)) + { + _deletedRelationsById.Remove(relation.Id); + } + + DsmElement consumer = relation.Consumer as DsmElement; + consumer.Dependencies.AddOutgoingRelation(relation); + + DsmElement provider = relation.Provider as DsmElement; + provider.Dependencies.AddIngoingRelation(relation); + + AddWeights(relation); + } + + private void UnregisterRelation(DsmRelation relation) + { + relation.IsDeleted = true; + _relationsById.Remove(relation.Id); + + _deletedRelationsById[relation.Id] = relation; + + DsmElement consumer = relation.Consumer as DsmElement; + consumer.Dependencies.RemoveOutgoingRelation(relation); + + DsmElement provider = relation.Provider as DsmElement; + provider.Dependencies.RemoveIngoingRelation(relation); + + RemoveWeights(relation); + } + + public void AddWeights(IDsmRelation relation) + { + DsmElement currentConsumer = relation.Consumer as DsmElement; + while (currentConsumer != null) + { + IDsmElement currentProvider = relation.Provider; + while (currentProvider != null) + { + if ((currentConsumer.Id != currentProvider.Id) && + !currentConsumer.IsRoot && + !currentProvider.IsRoot && + !currentConsumer.IsRecursiveChildOf(currentProvider) && + !currentProvider.IsRecursiveChildOf(currentConsumer)) + { + currentConsumer.Dependencies.AddDerivedWeight(currentProvider, relation.Weight); + } + currentProvider = currentProvider.Parent; + } + currentConsumer = currentConsumer.Parent as DsmElement; + } + } + + public void RemoveWeights(IDsmRelation relation) + { + DsmElement currentConsumer = relation.Consumer as DsmElement; + while (currentConsumer != null) + { + IDsmElement currentProvider = relation.Provider; + while (currentProvider != null) + { + if ((currentConsumer.Id != currentProvider.Id) && + !currentConsumer.IsRoot && + !currentProvider.IsRoot && + !currentConsumer.IsRecursiveChildOf(currentProvider) && + !currentProvider.IsRecursiveChildOf(currentConsumer)) + { + currentConsumer.Dependencies.RemoveDerivedWeight(currentProvider, relation.Weight); + } + currentProvider = currentProvider.Parent; + } + currentConsumer = currentConsumer.Parent as DsmElement; + } + } + + private void CountCycles(IDsmElement element, CycleType cycleType, ref int cycleCount) + { + foreach (IDsmElement consumer in element.Children) + { + foreach (IDsmElement provider in element.Children) + { + if (IsCyclicDependency(consumer, provider) == cycleType) + { + cycleCount++; + } + } + } + + foreach (IDsmElement child in element.Children) + { + CountCycles(child, cycleType, ref cycleCount); + } + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/NameRegistration.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/NameRegistration.cs new file mode 100644 index 00000000..0c98af98 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Core/NameRegistration.cs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using System.Diagnostics; + +namespace DsmSuite.DsmViewer.Model.Core +{ + /// + /// Collection of relation or element types. There can be max 256 different types. + /// Introduced to reduce memory usage by avoiding storing type string multiple times in memory. + /// + public class NameRegistration + { + private readonly Dictionary _registeredNames = new Dictionary(); + private readonly Dictionary _nameIds = new Dictionary(); + + public char RegisterName(string typeName) + { + Debug.Assert(_registeredNames.Count < 255); + if (_nameIds.ContainsKey(typeName)) + { + return _nameIds[typeName]; + } + else + { + char id = (char)_registeredNames.Count; + _registeredNames[id] = typeName; + _nameIds[typeName] = id; + return id; + } + } + + public string GetRegisteredName(char typeId) + { + if (_registeredNames.ContainsKey(typeId)) + { + return _registeredNames[typeId]; + } + else + { + return "unknown"; + } + } + + public IEnumerable GetRegisteredNames() + { + return _registeredNames.Values; + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/DsmSuite.DsmViewer.Model.csproj b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/DsmSuite.DsmViewer.Model.csproj new file mode 100644 index 00000000..4c95b7b6 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/DsmSuite.DsmViewer.Model.csproj @@ -0,0 +1,16 @@ + + + + net10.0 + Library + enable + + + + + + + + + + diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/CycleType.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/CycleType.cs new file mode 100644 index 00000000..8c41ac91 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/CycleType.cs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +namespace DsmSuite.DsmViewer.Model.Interfaces +{ + public enum CycleType + { + None, + System, + Hierarchical + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/IDsmAction.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/IDsmAction.cs new file mode 100644 index 00000000..00874648 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/IDsmAction.cs @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +namespace DsmSuite.DsmViewer.Model.Interfaces +{ + public interface IDsmAction + { + int Id { get; } + + string Type { get; } + + IReadOnlyDictionary Data { get; } + + /// + /// Sub-actions or null. + /// + IEnumerable Actions { get; } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/IDsmElement.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/IDsmElement.cs new file mode 100644 index 00000000..11527bfe --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/IDsmElement.cs @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +namespace DsmSuite.DsmViewer.Model.Interfaces +{ + public interface IDsmElement : IComparable + { + /// + /// Unique and non-modifiable Number identifying the element. + /// + int Id { get; } + + /// + /// Number identifying sequential order of the element in element tree. + /// + int Order { get; } + + /// + /// Type of element. + /// + string Type { get; } + + /// + /// Name of the element. + /// + string Name { get; } + + // Named properties found for this element + IDictionary Properties { get; } + + // Property names found across all elements + IEnumerable DiscoveredElementPropertyNames(); + + /// + /// Full name of the element based on its position in the element hierarchy + /// + string Fullname { get; } + + string GetRelativeName(IDsmElement element); + + bool IsDeleted { get; } + bool IsBookmarked { get; set; } + bool IsRoot { get; } + + /// + /// Has the element any children that are in the tree (see ). + /// + bool HasChildren { get; } + + /// + /// Tree children of the element (see ). + /// + IList Children { get; } + + IList AllChildren { get; } + + int IndexOfChild(IDsmElement child); + + bool ContainsChildWithName(string name); + + /// + /// Parent of the element. + /// + IDsmElement Parent { get; } + + /// + /// Is the selected element a recursive child of this element. + /// + bool IsRecursiveChildOf(IDsmElement element); + + /// + /// Is the element expanded in the viewer. This is only meaningful for elements with children.
+ /// If an element is expanded, each of its children has a line in the matrix and the element + /// itself hasn't, but is used to vertically group the children.
+ /// If an element is not expanded, its children are not displayed and the element itself has + /// a row in the matrix.
+ /// This property is only relevant and should only be changed for elements that are in the + /// tree (see ). + ///
+ bool IsExpanded { get; set; } + + /// + /// Set or clear the IsExpanded property recursively for elements in the tree. + /// + void ExpandRecursively(bool expanded); + + /// + /// Is the element match in search. + /// + bool IsMatch { get; set; } + + /// + /// Is the element included in the tree. The tree is the set of elements that are in scope + /// for the viewer, i.e. that are not filtered out. + /// + bool IsIncludedInTree { get; set; } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/IDsmModel.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/IDsmModel.cs new file mode 100644 index 00000000..dd28e147 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/IDsmModel.cs @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.Common.Model.Interface; +using DsmSuite.Common.Util; + +namespace DsmSuite.DsmViewer.Model.Interfaces +{ + /// + /// Represents a domain structure matrix. A Dsm model consists of a hierarchy of elements + /// , relations between elements and a + /// sequence of actions that were executed on the model. + /// + /// + /// Models can be saved and loaded. Element and relations can have types (strings) assigned + /// to them. Actions are strings as well. The model is agnostic about the meaning of + /// actions and element/relation types. + /// + public interface IDsmModel + { + // Cleanup + void Clear(); + + // Model persistency + string ModelFilename { get; } + bool IsCompressed { get; } + void LoadModel(string dsmFilename, IProgress progress); + void SaveModel(string dsmFilename, bool compressFile, IProgress progress); + + // Meta data + IMetaDataItem AddMetaData(string group, string name, string value); + + // Element editing + IDsmElement AddElement(int id, string name, string type, IDictionary properties, int order); + IDsmElement AddElement(string name, string type, int? parentId, int? index, IDictionary properties); + void RemoveElement(int elementId); + void UnremoveElement(int elementId); + void ChangeElementName(IDsmElement element, string name); + void ChangeElementType(IDsmElement element, string type); + bool IsChangeElementParentAllowed(IDsmElement element, IDsmElement parent); + void ChangeElementParent(IDsmElement element, IDsmElement parent, int index); + void ReorderChildren(IDsmElement element, ISortResult sortResult); + IDsmElement NextSibling(IDsmElement element); + IDsmElement PreviousSibling(IDsmElement element); + bool Swap(IDsmElement first, IDsmElement second); + void AssignElementOrder(); + /// + /// Includes or excludes the children and parents of the given element in the tree. + /// + void IncludeInTree(IDsmElement element, bool included); + + // Element queries + IDsmElement GetElementById(int id); + IDsmElement GetDeletedElementById(int id); + IDsmElement GetElementByFullname(string fullname); + IList SearchElements(string searchText, IDsmElement searchInElement, bool caseSensitive, string elementTypeFilter, bool markMatchingElements); + IDsmElement RootElement { get; } + IEnumerable GetElementTypes(); + IEnumerable GetElements(); + int GetElementCount(); + + // Relation editing + IDsmRelation AddRelation(IDsmElement consumer, IDsmElement provider, string type, int weight, IDictionary properties); + IDsmRelation AddRelation(int id, IDsmElement consumer, IDsmElement provider, string type, int weight, IDictionary properties); + void ChangeRelationType(IDsmRelation relation, string type); + void ChangeRelationWeight(IDsmRelation relation, int weight); + void RemoveRelation(int relationId); + void UnremoveRelation(int relationId); + IEnumerable GetRelationTypes(); + + // Relation queries + int GetDependencyWeight(IDsmElement consumer, IDsmElement provider); + int GetDirectDependencyWeight(IDsmElement consumer, IDsmElement provider); + CycleType IsCyclicDependency(IDsmElement consumer, IDsmElement provider); + IDsmRelation GetRelationById(int relationId); + IDsmRelation GetDeletedRelationById(int relationId); + IEnumerable GetRelations(); + IDsmRelation FindRelation(IDsmElement consumer, IDsmElement provider, string type); + IEnumerable FindRelations(IDsmElement consumer, IDsmElement provider); + int GetRelationCount(IDsmElement consumer, IDsmElement provider); + IEnumerable FindIngoingRelations(IDsmElement element); + IEnumerable FindOutgoingRelations(IDsmElement element); + IEnumerable FindInternalRelations(IDsmElement element); + IEnumerable FindExternalRelations(IDsmElement element); + int GetHierarchicalCycleCount(IDsmElement element); + int GetSystemCycleCount(IDsmElement element); + // Actions + IDsmAction AddAction(string type, IReadOnlyDictionary data, + IEnumerable actions); + void ClearActions(); + IEnumerable GetActions(); + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/IDsmRelation.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/IDsmRelation.cs new file mode 100644 index 00000000..b93ecab3 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/IDsmRelation.cs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +namespace DsmSuite.DsmViewer.Model.Interfaces +{ + public interface IDsmRelation + { + /// + /// Unique and non-modifiable Number identifying the relation. + /// + int Id { get; } + + /// + /// The consumer element. + /// + IDsmElement Consumer { get; } + + /// + /// The provider element. + /// + IDsmElement Provider { get; } + + /// + /// Type of relation. + /// + string Type { get; } + + /// + /// Strength or weight of the relation + /// + int Weight { get; } + + bool IsDeleted { get; } + + // Named properties found for this relation + IDictionary Properties { get; } + + // Property names found across all relations + IEnumerable DiscoveredRelationPropertyNames(); + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/ISortResult.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/ISortResult.cs new file mode 100644 index 00000000..43d5d3ee --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/ISortResult.cs @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +namespace DsmSuite.DsmViewer.Model.Interfaces +{ + public interface ISortResult + { + int GetIndex(int currentIndex); + int GetNumberOfElements(); + bool IsValid { get; } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/RelationDirection.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/RelationDirection.cs new file mode 100644 index 00000000..5163811c --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/RelationDirection.cs @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +namespace DsmSuite.DsmViewer.Model.Interfaces +{ + public enum RelationDirection + { + Ingoing, + Outgoing, + Both + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/RelationScope.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/RelationScope.cs new file mode 100644 index 00000000..07437c4a --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Interfaces/RelationScope.cs @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +namespace DsmSuite.DsmViewer.Model.Interfaces +{ + public enum RelationScope + { + Internal, + External, + Both + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/ModelClasses.cd b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/ModelClasses.cd new file mode 100644 index 00000000..d7e7079c --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/ModelClasses.cd @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ApAkRACAIJQBAGSAAAILQAEAoIgjJYCExkQgBARgFKA= + Core\DsmModel.cs + + + + + + + + + + + + + + + + + AAMSAQBAAAwAAASAAAgMQEQQEAIBQIAgARAEAQgGAQg= + Core\DsmElement.cs + + + + + + + + + + + + + + + AIAgAICAAIAdAHCAAAIAEAlAIAgDJAIGBEAAIABADIA= + Core\DsmElementModel.cs + + + + + + + + + + IhAERACAAAQAACQBYAAiAQwAgQA4AYCMggBgBAVAAAA= + Core\DsmRelationModel.cs + + + + + + + + + + AgAAAAAAAAAAAAAAMAAAAIAAAAAABAAAAAAAAABgAAA= + Core\DsmActionModel.cs + + + + + + + + + + + + + + + + + + + + AAECAAAAAAAEAAAAAAAAAAAAACBAQAAIAQACAQACAAI= + Core\DsmRelation.cs + + + + + + + + + + + AAACAAAAAAAAAAAAAAAAAAAAAAAAABAAAQAAAAAAAAA= + Core\DsmAction.cs + + + + + + + AAAAQgQABAAAAABAAAAIBABIABBYAAAAwAgBAAAAAAA= + Core\DsmDependencies.cs + + + + + + ApAkRACAIIQBAGSAAAILQAEAoIgjJYCEhkAgBARgBAA= + Interfaces\IDsmModel.cs + + + + + + AAMCAQBAAAgAAAAAAAAEQEQAEAAAAAAAARAEAQgGAAg= + Interfaces\IDsmElement.cs + + + + + + + + + AAACAAAAAAAAAAAAAAAAAAAAAAAAABAAAQAAAAAAAAA= + Interfaces\IDsmAction.cs + + + + + + AAACAAAAAAAEAAAAAAAAAAAAACBAAAAAAQACAQACAAA= + Interfaces\IDsmRelation.cs + + + + \ No newline at end of file diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Persistency/DsmModelFile.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Persistency/DsmModelFile.cs new file mode 100644 index 00000000..694af3e7 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Persistency/DsmModelFile.cs @@ -0,0 +1,592 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.Common.Model.Interface; +using DsmSuite.Common.Model.Persistency; +using DsmSuite.Common.Util; +using DsmSuite.DsmViewer.Model.Core; +using DsmSuite.DsmViewer.Model.Interfaces; +using System.Xml; + +namespace DsmSuite.DsmViewer.Model.Persistency +{ + public class DsmModelFile + { + private const string RootXmlNode = "dsmmodel"; + private const string ModelElementCountXmlAttribute = "elementCount"; + private const string ModelRelationCountXmlAttribute = "relationCount"; + private const string ModelActionCountXmlAttribute = "actionCount"; + + private const string MetaDataGroupXmlNode = "metadatagroup"; + private const string MetaDataGroupNameXmlAttribute = "name"; + + private const string MetaDataXmlNode = "metadata"; + private const string MetaDataItemNameXmlAttribute = "name"; + private const string MetaDataItemValueXmlAttribute = "value"; + + private const string ElementGroupXmlNode = "elements"; + + private const string ElementXmlNode = "element"; + private const string ElementIdXmlAttribute = "id"; + private const string ElementOrderXmlAttribute = "order"; + private const string ElementNameXmlAttribute = "name"; + private const string ElementTypeXmlAttribute = "type"; + private const string ElementExpandedXmlAttribute = "expanded"; + private const string ElementParentXmlAttribute = "parent"; + private const string ElementDeletedXmlAttribute = "deleted"; + private const string ElementBookmarkedXmlAttribute = "bookmarked"; + private const string ElementInTreeXmlAttribute = "tree"; + + private const string RelationGroupXmlNode = "relations"; + + private const string RelationXmlNode = "relation"; + private const string RelationIdXmlAttribute = "id"; + private const string RelationFromXmlAttribute = "from"; + private const string RelationToXmlAttribute = "to"; + private const string RelationTypeXmlAttribute = "type"; + private const string RelationWeightXmlAttribute = "weight"; + private const string RelationDeletedXmlAttribute = "deleted"; + + private const string ActionGroupXmlNode = "actions"; + + private const string ActionXmlNode = "action"; + private const string ActionIdXmlAttribute = "id"; + private const string ActionTypeXmlAttribute = "type"; + private const string ActionDataXmlNode = "data"; + + private readonly string _filename; + private readonly IMetaDataModelFileCallback _metaDataModelCallback; + private readonly IDsmElementModelFileCallback _elementModelCallback; + private readonly IDsmRelationModelFileCallback _relationModelCallback; + private readonly IDsmActionModelFileCallback _actionModelCallback; + private int _totalElementCount; + private int _progressedElementCount; + private int _totalRelationCount; + private int _progressedRelationCount; + private int _totalActionCount; + private int _progressedActionCount; + private int _progress; + private string _progressActionText; + + public DsmModelFile(string filename, + IMetaDataModelFileCallback metaDataModelCallback, + IDsmElementModelFileCallback elementModelCallback, + IDsmRelationModelFileCallback relationModelCallback, + IDsmActionModelFileCallback actionModelCallback) + { + _filename = filename; + _metaDataModelCallback = metaDataModelCallback; + _elementModelCallback = elementModelCallback; + _relationModelCallback = relationModelCallback; + _actionModelCallback = actionModelCallback; + } + + public void Save(bool compressed, IProgress progress) + { + _progressActionText = "Saving dsm model"; + CompressedFile modelFile = new CompressedFile(_filename); + modelFile.WriteFile(WriteDsmXml, progress, compressed); + UpdateProgress(progress, true); + } + + public void Load(IProgress progress) + { + _progressActionText = "Loading dsm model"; + CompressedFile modelFile = new CompressedFile(_filename); + modelFile.ReadFile(ReadDsmXml, progress); + UpdateProgress(progress, true); + } + + public bool IsCompressedFile() + { + CompressedFile modelFile = new CompressedFile(_filename); + return modelFile.IsCompressed; + } + + private void WriteDsmXml(Stream stream, IProgress progress) + { + XmlWriterSettings settings = new XmlWriterSettings + { + Indent = true, + IndentChars = (" ") + }; + + using (XmlWriter writer = XmlWriter.Create(stream, settings)) + { + writer.WriteStartDocument(); + writer.WriteStartElement(RootXmlNode); + { + WriteModelAttributes(writer); + WriteMetaData(writer); + WriteElements(writer, progress); + WriteRelations(writer, progress); + WriteActions(writer, progress); + } + writer.WriteEndElement(); + writer.WriteEndDocument(); + } + } + + private void ReadDsmXml(Stream stream, IProgress progress) + { + using (XmlReader xReader = XmlReader.Create(stream)) + { + while (xReader.Read()) + { + switch (xReader.NodeType) + { + case XmlNodeType.Element: + ReadModelAttributes(xReader); + ReadMetaDataGroup(xReader); + ReadElement(xReader, progress); + ReadRelation(xReader, progress); + ReadActionGroup(xReader, _actionModelCallback, progress); + break; + case XmlNodeType.Text: + break; + case XmlNodeType.EndElement: + break; + } + } + } + } + + private void WriteModelAttributes(XmlWriter writer) + { + _totalElementCount = _elementModelCallback.GetExportedElementCount(); + writer.WriteAttributeString(ModelElementCountXmlAttribute, _totalElementCount.ToString()); + _progressedElementCount = 0; + + _totalRelationCount = _relationModelCallback.GetExportedRelationCount(); + writer.WriteAttributeString(ModelRelationCountXmlAttribute, _totalRelationCount.ToString()); + _progressedRelationCount = 0; + + _totalActionCount = _actionModelCallback.GetExportedActionCount(); + writer.WriteAttributeString(ModelActionCountXmlAttribute, _totalActionCount.ToString()); + _progressedActionCount = 0; + } + + private void ReadModelAttributes(XmlReader xReader) + { + if (xReader.Name == RootXmlNode) + { + int? elementCount = ParseInt(xReader.GetAttribute(ModelElementCountXmlAttribute)); + int? relationCount = ParseInt(xReader.GetAttribute(ModelRelationCountXmlAttribute)); + int? actionCount = ParseInt(xReader.GetAttribute(ModelActionCountXmlAttribute)); + + _totalElementCount = elementCount ?? 0; + _progressedElementCount = 0; + _totalRelationCount = relationCount ?? 0; + _progressedRelationCount = 0; + _totalActionCount = actionCount ?? 0; + _progressedActionCount = 0; + } + } + + private void WriteMetaData(XmlWriter writer) + { + foreach (string group in _metaDataModelCallback.GetExportedMetaDataGroups()) + { + WriteMetaDataGroup(writer, group); + } + } + + private void WriteMetaDataGroup(XmlWriter writer, string group) + { + writer.WriteStartElement(MetaDataGroupXmlNode); + writer.WriteAttributeString(MetaDataGroupNameXmlAttribute, group); + + foreach (IMetaDataItem metaDataItem in _metaDataModelCallback.GetExportedMetaDataGroupItems(group)) + { + writer.WriteStartElement(MetaDataXmlNode); + writer.WriteAttributeString(MetaDataItemNameXmlAttribute, metaDataItem.Name); + writer.WriteAttributeString(MetaDataItemValueXmlAttribute, metaDataItem.Value); + writer.WriteEndElement(); + } + writer.WriteEndElement(); + } + + private void ReadMetaDataGroup(XmlReader xReader) + { + if (xReader.Name == MetaDataGroupXmlNode) + { + string group = xReader.GetAttribute(MetaDataGroupNameXmlAttribute); + XmlReader xMetaDataReader = xReader.ReadSubtree(); + while (xMetaDataReader.Read()) + { + if (xMetaDataReader.Name == MetaDataXmlNode) + { + string name = xMetaDataReader.GetAttribute(MetaDataItemNameXmlAttribute); + string value = xMetaDataReader.GetAttribute(MetaDataItemValueXmlAttribute); + if ((name != null) && (value != null)) + { + _metaDataModelCallback.ImportMetaDataItem(group, name, value); + } + } + } + } + } + + private void WriteElements(XmlWriter writer, IProgress progress) + { + writer.WriteStartElement(ElementGroupXmlNode); + foreach (IDsmElement element in _elementModelCallback.RootElement.AllChildren) + { + WriteElement(writer, element, progress); + } + writer.WriteEndElement(); + } + + private void WriteElement(XmlWriter writer, IDsmElement element, IProgress progress) + { + writer.WriteStartElement(ElementXmlNode); + writer.WriteAttributeString(ElementIdXmlAttribute, element.Id.ToString()); + writer.WriteAttributeString(ElementOrderXmlAttribute, element.Order.ToString()); + writer.WriteAttributeString(ElementNameXmlAttribute, element.Name); + writer.WriteAttributeString(ElementTypeXmlAttribute, element.Type); + writer.WriteAttributeString(ElementExpandedXmlAttribute, element.IsExpanded.ToString()); + if (element.IsDeleted) + { + writer.WriteAttributeString(ElementDeletedXmlAttribute, "true"); + } + if (element.IsBookmarked) + { + writer.WriteAttributeString(ElementBookmarkedXmlAttribute, "true"); + } + writer.WriteAttributeString(ElementInTreeXmlAttribute, element.IsIncludedInTree.ToString()); + if ((element.Parent != null) && (element.Parent.Id > 0)) + { + writer.WriteAttributeString(ElementParentXmlAttribute, element.Parent.Id.ToString()); + } + if (element.Properties != null) + { + foreach (KeyValuePair elementProperty in element.Properties) + { + writer.WriteAttributeString(elementProperty.Key, elementProperty.Value); + } + } + writer.WriteEndElement(); + + _progressedElementCount++; + UpdateProgress(progress, false); + + foreach (IDsmElement child in element.AllChildren) + { + WriteElement(writer, child, progress); + } + } + + private void ReadElement(XmlReader xReader, IProgress progress) + { + if (xReader.Name == ElementXmlNode) + { + int? id = null; + int? order = null; + string name = ""; + string type = ""; + bool expanded = false; + int? parent = null; + bool deleted = false; + bool bookmarked = false; + bool intree = true; + + Dictionary elementProperties = new Dictionary(); + for (int attInd = 0; attInd < xReader.AttributeCount; attInd++) + { + xReader.MoveToAttribute(attInd); + switch (xReader.Name) + { + case ElementIdXmlAttribute: + id = ParseInt(xReader.Value); + break; + case ElementOrderXmlAttribute: + order = ParseInt(xReader.Value); + break; + case ElementNameXmlAttribute: + name = xReader.Value; + break; + case ElementTypeXmlAttribute: + type = xReader.Value; + break; + case ElementExpandedXmlAttribute: + expanded = ParseBool(xReader.Value); + break; + case ElementParentXmlAttribute: + parent = ParseInt(xReader.Value); + break; + case ElementDeletedXmlAttribute: + deleted = ParseBool(xReader.Value); + break; + case ElementBookmarkedXmlAttribute: + bookmarked = ParseBool(xReader.Value); + break; + case ElementInTreeXmlAttribute: + intree = ParseBool(xReader.Value); + break; + default: + if (!string.IsNullOrEmpty(xReader.Value)) + { + elementProperties[xReader.Name] = xReader.Value; + } + break; + } + } + + if (id.HasValue && order.HasValue) + { + IDsmElement element = _elementModelCallback.ImportElement( + id.Value, name, type, + elementProperties.Count > 0 ? elementProperties : null, + order.Value, expanded, parent, deleted + ); + element.IsBookmarked = bookmarked; + element.IsIncludedInTree = intree; + } + + _progressedElementCount++; + UpdateProgress(progress, false); + } + } + + private void WriteRelations(XmlWriter writer, IProgress progress) + { + writer.WriteStartElement(RelationGroupXmlNode); + foreach (IDsmRelation relation in _relationModelCallback.GetExportedRelations()) + { + WriteRelation(writer, relation, progress); + } + writer.WriteEndElement(); + } + + private void WriteRelation(XmlWriter writer, IDsmRelation relation, IProgress progress) + { + writer.WriteStartElement(RelationXmlNode); + writer.WriteAttributeString(RelationIdXmlAttribute, relation.Id.ToString()); + writer.WriteAttributeString(RelationFromXmlAttribute, relation.Consumer.Id.ToString()); + writer.WriteAttributeString(RelationToXmlAttribute, relation.Provider.Id.ToString()); + writer.WriteAttributeString(RelationTypeXmlAttribute, relation.Type); + writer.WriteAttributeString(RelationWeightXmlAttribute, relation.Weight.ToString()); + if (relation.IsDeleted) + { + writer.WriteAttributeString(RelationDeletedXmlAttribute, "true"); + } + if (relation.Properties != null) + { + foreach (KeyValuePair relationProperty in relation.Properties) + { + writer.WriteAttributeString(relationProperty.Key, relationProperty.Value); + } + } + writer.WriteEndElement(); + + _progressedRelationCount++; + UpdateProgress(progress, false); + } + + private void ReadRelation(XmlReader xReader, IProgress progress) + { + if (xReader.Name == RelationXmlNode) + { + int? id = null; + int? consumerId = null; + int? providerId = null; + string type = ""; + int? weight = null; + bool deleted = false; + + Dictionary relationProperties = new Dictionary(); + for (int attInd = 0; attInd < xReader.AttributeCount; attInd++) + { + xReader.MoveToAttribute(attInd); + switch (xReader.Name) + { + case RelationIdXmlAttribute: + id = ParseInt(xReader.Value); + break; + case RelationFromXmlAttribute: + consumerId = ParseInt(xReader.Value); + break; + case RelationToXmlAttribute: + providerId = ParseInt(xReader.Value); + break; + case RelationTypeXmlAttribute: + type = xReader.Value; + break; + case RelationWeightXmlAttribute: + weight = ParseInt(xReader.Value); + break; + case RelationDeletedXmlAttribute: + deleted = ParseBool(xReader.Value); + break; + default: + if (!string.IsNullOrEmpty(xReader.Value)) + { + relationProperties[xReader.Name] = xReader.Value; + } + break; + } + } + + if (id.HasValue && consumerId.HasValue && providerId.HasValue && weight.HasValue) + { + IDsmElement consumer = _elementModelCallback.FindElementById(consumerId.Value); + IDsmElement provider = _elementModelCallback.FindElementById(providerId.Value); + + if (relationProperties.Count > 0) + { + _relationModelCallback.ImportRelation(id.Value, consumer, provider, type, weight.Value, relationProperties, deleted); + } + else + { + _relationModelCallback.ImportRelation(id.Value, consumer, provider, type, weight.Value, null, deleted); + } + } + + _progressedRelationCount++; + UpdateProgress(progress, false); + } + } + + private void WriteActions(XmlWriter writer, IProgress progress) + { + WriteActionGroup(writer, _actionModelCallback.GetExportedActions(), progress); + } + + private void WriteActionGroup(XmlWriter writer, IEnumerable actions, + IProgress progress) + { + writer.WriteStartElement(ActionGroupXmlNode); + foreach (IDsmAction action in actions) + WriteAction(writer, action, progress); + writer.WriteEndElement(); + } + + + private void WriteAction(XmlWriter writer, IDsmAction action, IProgress progress) + { + writer.WriteStartElement(ActionXmlNode); + writer.WriteAttributeString(ActionIdXmlAttribute, action.Id.ToString()); + writer.WriteAttributeString(ActionTypeXmlAttribute, action.Type); + + writer.WriteStartElement(ActionDataXmlNode); + foreach (var d in action.Data) + { + writer.WriteAttributeString(d.Key, d.Value); + } + writer.WriteEndElement(); + + if (action.Actions != null) + WriteActionGroup(writer, action.Actions, progress); + + writer.WriteEndElement(); + + _progressedActionCount++; + UpdateProgress(progress, false); + } + + private void ReadActionGroup(XmlReader xReader, IDsmActionModelFileCallback actionModel, + IProgress progress) + { + if (xReader.Name == ActionGroupXmlNode) + { + XmlReader xActionsReader = xReader.ReadSubtree(); + while (xActionsReader.Read()) + { + if (xActionsReader.Name == ActionXmlNode) + ReadAction(xActionsReader, actionModel, progress); + } + } + } + + private void ReadAction(XmlReader xReader, IDsmActionModelFileCallback actionModel, + IProgress progress) + { + if (xReader.Name == ActionXmlNode) + { + int? id = ParseInt(xReader.GetAttribute(ActionIdXmlAttribute)); + string type = xReader.GetAttribute(ActionTypeXmlAttribute); + Dictionary data = new Dictionary(); + DsmActionModel subactions = null; + + XmlReader xActionDataReader = xReader.ReadSubtree(); + while (xActionDataReader.Read()) + { + switch (xActionDataReader.Name) + { + case ActionDataXmlNode: + while (xActionDataReader.MoveToNextAttribute()) + data[xReader.Name] = xReader.Value; + xActionDataReader.MoveToElement(); + break; + case ActionGroupXmlNode: + subactions = new DsmActionModel(); + ReadActionGroup(xActionDataReader, subactions, progress); + break; + } + } + + if (id.HasValue) + { + actionModel.ImportAction(id.Value, type, data, subactions?.GetExportedActions()); + } + + _progressedActionCount++; + UpdateProgress(progress, false); + } + } + + private void UpdateProgress(IProgress progress, bool done) + { + if (progress != null) + { + int totalItemCount = _totalElementCount + _totalRelationCount + _totalActionCount; + int progressedItemCount = _progressedElementCount + _progressedRelationCount + _progressedActionCount; + + int currentProgress = 0; + if (totalItemCount > 0) + { + currentProgress = progressedItemCount * 100 / totalItemCount; + } + + if ((_progress != currentProgress) || done) + { + _progress = currentProgress; + + ProgressInfo progressInfoInfo = new ProgressInfo + { + ActionText = _progressActionText, + Percentage = _progress, + TotalItemCount = totalItemCount, + CurrentItemCount = progressedItemCount, + ItemType = "items", + Done = done + }; + + progress.Report(progressInfoInfo); + } + } + } + + private int? ParseInt(string value) + { + int? result = null; + + int parsedValued; + if (int.TryParse(value, out parsedValued)) + { + result = parsedValued; + } + return result; + } + + private bool ParseBool(string value) + { + bool result = false; + + bool parsedValued; + if (bool.TryParse(value, out parsedValued)) + { + result = parsedValued; + } + return result; + } + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Persistency/IDsmActionModelFileCallback.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Persistency/IDsmActionModelFileCallback.cs new file mode 100644 index 00000000..049c189d --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Persistency/IDsmActionModelFileCallback.cs @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Model.Persistency +{ + /// + /// Specifies the methods for adding actions to the model and reading them. + /// DsmModelFile call methods in this to write actions obtained from XML, or to determine which actions + /// to write to XML. + /// + public interface IDsmActionModelFileCallback + { + /// Create a new action with the given properties. + /// The new action. + IDsmAction ImportAction(int id, string type, IReadOnlyDictionary data, + IEnumerable actions); + + /// Return all actions in the model. + IEnumerable GetExportedActions(); + + /// Return the number of actions in the model. + int GetExportedActionCount(); + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Persistency/IDsmElementModelFileCallback.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Persistency/IDsmElementModelFileCallback.cs new file mode 100644 index 00000000..a0ea778a --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Persistency/IDsmElementModelFileCallback.cs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Model.Persistency +{ + public interface IDsmElementModelFileCallback + { + IDsmElement FindElementById(int elementId); + IDsmElement ImportElement(int id, string name, string type, IDictionary properties, int order, bool expanded, int? parent, bool deleted); + IDsmElement RootElement { get; } + int GetExportedElementCount(); + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Persistency/IDsmRelationModelFileCallback.cs b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Persistency/IDsmRelationModelFileCallback.cs new file mode 100644 index 00000000..d60aa1e4 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.Model/Persistency/IDsmRelationModelFileCallback.cs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +using DsmSuite.DsmViewer.Model.Interfaces; + +namespace DsmSuite.DsmViewer.Model.Persistency +{ + public interface IDsmRelationModelFileCallback + { + IDsmRelation ImportRelation(int id, IDsmElement consumer, IDsmElement provider, string type, int weight, IDictionary properties, bool deleted); + + IEnumerable GetExportedRelations(); + int GetExportedRelationCount(); + } +} diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.View/DsmSuite.DsmViewer.View.csproj b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.View/DsmSuite.DsmViewer.View.csproj new file mode 100644 index 00000000..8d12758e --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.View/DsmSuite.DsmViewer.View.csproj @@ -0,0 +1,28 @@ + + + + Library + net10.0-windows + enable + true + + + + Resources/DSM.ico + + + + + + + + + + + + + + + + + diff --git a/ThirdParty/DsmSuite/DsmSuite.DsmViewer.View/Editing/ElementEditDialog.xaml b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.View/Editing/ElementEditDialog.xaml new file mode 100644 index 00000000..af3a6631 --- /dev/null +++ b/ThirdParty/DsmSuite/DsmSuite.DsmViewer.View/Editing/ElementEditDialog.xaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + +