diff --git a/src/AasxCore.Samm2_2_0/SammClasses.cs b/src/AasxCore.Samm2_2_0/SammClasses.cs index cfa10797..7917d80a 100644 --- a/src/AasxCore.Samm2_2_0/SammClasses.cs +++ b/src/AasxCore.Samm2_2_0/SammClasses.cs @@ -489,8 +489,6 @@ public bool AddOrIgnore(string prefix, string uri) if (input.StartsWith(mi.Uri)) { var pf = mi.Prefix; - if (pf == ":") - pf = "this:"; return pf + input.Substring(mi.Uri.Length); } @@ -1115,12 +1113,8 @@ public IEnumerable DescendOnce() // own [SammPropertyUri("bamm:properties")] [SammCollectionContentUri("bamm:property")] - public List Properties { get; set; } - - public Entity() - { - Properties = new List(); - } + public List Properties { get; set; } + = new List(); } /// @@ -1169,14 +1163,14 @@ public IEnumerable DescendOnce() /// [SammPropertyUri("bamm:events")] [SammCollectionContentUri("bamm:event")] - public List Events { get; set; } = new List(); + public List Events { get; set; } = new List(); /// /// An Operation represents an action that can be triggered on the Aspect. /// [SammPropertyUri("bamm:operations")] [SammCollectionContentUri("bamm:operation")] - public List Operations { get; set; } = new List(); + public List Operations { get; set; } = new List(); } public class Unit : ModelElement, ISammSelfDescription diff --git a/src/AasxPackageExplorer/debug.MIHO.script b/src/AasxPackageExplorer/debug.MIHO.script index 41ab7670..45f8d475 100644 --- a/src/AasxPackageExplorer/debug.MIHO.script +++ b/src/AasxPackageExplorer/debug.MIHO.script @@ -14,4 +14,5 @@ Select("ConceptDescription", "First"); // Select("ConceptDescription", "Next"); // Select("ConceptDescription", "Next"); // Select("ConceptDescription", "Next"); -Tool("sammaspectexport", "File", "C:\\HOMI\\Develop\\Aasx\\repo\\out.ttl"); \ No newline at end of file +// Tool("sammaspectexport", "File", "C:\\HOMI\\Develop\\Aasx\\repo\\out.ttl"); +Tool("submodelinstancefromsammaspect"); \ No newline at end of file diff --git a/src/AasxPackageLogic/DispEditHelperSammModules.cs b/src/AasxPackageLogic/DispEditHelperSammModules.cs index 1608e6a7..a369db5e 100644 --- a/src/AasxPackageLogic/DispEditHelperSammModules.cs +++ b/src/AasxPackageLogic/DispEditHelperSammModules.cs @@ -40,6 +40,7 @@ This source code may use other Open Source software components (see LICENSE.txt) using System.Web.Services.Description; using static AasxPackageLogic.DispEditHelperBasics; using System.Collections; +using static Lucene.Net.Documents.Field; namespace AasxPackageLogic { @@ -502,6 +503,46 @@ public void SammExtensionHelperAddCompleteModelElement( createInstance: (sr) => new OptionalSammReference(sr)); } + // List of LangString + if (pii.PropertyType.IsAssignableTo(typeof(List))) + { + // space + this.AddVerticalSpace(stack); + + // get data + var lls = (List)pii.GetValue(sammInst); + + // handle null + Action> lambdaSetValue = (v) => + { + var back = v?.Select((ls) => new Samm.LangString(ls)).ToList(); + pii.SetValue(sammInst, back); + setValue?.Invoke(sammInst); + }; + + if (this.SafeguardAccess(stack, repo, lls, "" + pii.Name + ":", + "Create data element!", + v => + { + lambdaSetValue(new List()); + return new AnyUiLambdaActionRedrawEntity(); + })) + { + // get values + var forth = lls?.Select( + (ls) => (new Aas.LangStringTextType(ls.Language, ls.Text)) + as Aas.ILangStringTextType).ToList(); + + // edit fields + AddKeyListLangStr( + stack, "" + pii.Name, forth, repo, relatedReferable, + emitCustomEvent: (rf) => + { + lambdaSetValue(forth); + }); + } + } + // NamespaceMap if (pii.PropertyType.IsAssignableTo(typeof(Samm.NamespaceMap))) { @@ -1258,7 +1299,10 @@ public List ImportRdfCollection( if (collPtr != null && (collPtr.NodeType == NodeType.Uri || collPtr.NodeType == NodeType.Literal)) { // only a single member is given - lsr.Add(createInstance?.Invoke(RdfHelper.GetTerminalStrValue(collPtr), false)); + var litVal = RdfHelper.GetTerminalStrValue(collPtr); + if (litVal?.HasContent() == true + && litVal != Samm.Constants.RdfCollNil) + lsr.Add(createInstance?.Invoke(litVal, false)); } else { @@ -2187,4 +2231,341 @@ public void ExportSammModelFromConceptDescription( System.IO.File.WriteAllText(fn, textAll); } } + + /// + /// This class provides generators for Qualifiers, Extension etc. + /// in order to express preset-based information e.g. Cardinality + /// + public static class AasPresetHelper + { + /// + /// Semantically different, but factually equal to FormMultiplicity + /// + public enum SmtCardinality { ZeroToOne = 0, One, ZeroToMany, OneToMany }; + + public static Aas.IQualifier CreateQualifierSmtCardinality(SmtCardinality card) + { + return new Aas.Qualifier( + type: "SMT/Cardinality", + valueType: DataTypeDefXsd.String, + kind: QualifierKind.TemplateQualifier, + semanticId: new Aas.Reference(ReferenceTypes.ExternalReference, + (new Aas.IKey[] { + new Aas.Key(KeyTypes.GlobalReference, + "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0") + }).ToList()), + value: "" + card); + } + } + + /// + /// This class provides a layered access to a list (a IEnumerable) of + /// Aas.IIdentifiable. This access could be provided by each time + /// blindly iterating through the list or by caching the Ids in a + /// dictionary. Result can be access immedeatily or selected by an + /// lambda, so that only portions of the Identifiable are directly available + /// and strongly typed. + /// + /// Subtype of Aas.IIdentifiable + /// Result type, selected by an lambda + public class IdentifiableLookupStore where I : Aas.IIdentifiable where ME : class + { + protected IEnumerable _originalData = null; + + protected Func _lambdaSelectResult = null; + + protected MultiValueDictionary _lookup = null; + + protected bool IsValidForDict() => + _originalData != null && _lambdaSelectResult != null && _lookup != null; + + public void StartDictionaryAccess( + IEnumerable originalData, + Func lambdaSelectResult) + { + // remember + _originalData = originalData; + _lambdaSelectResult = lambdaSelectResult; + + // create the dictionary + _lookup = new MultiValueDictionary(); + if (!IsValidForDict()) + { + _lookup = null; + return; + } + + // populate + foreach (var idf in _originalData) + if (idf?.Id != null) + _lookup.Add(idf.Id, idf); + } + + /// + /// Lookup all elements for id idKey and + /// return the declared Identifiable subtype. + /// + public IEnumerable LookupAllIdent(string idKey) + { + if (idKey == null || !IsValidForDict() || !_lookup.ContainsKey(idKey)) + yield break; + + foreach (var idf in _lookup[idKey]) + yield return idf; + } + + /// + /// Lookup first element for id idKey and + /// return the declared Identifiable subtype. Else: return null + /// + public I LookupFirstIdent(string idKey) + { + return LookupAllIdent(idKey).FirstOrDefault(); + } + + /// + /// Lookup all elements for id idKey and + /// return the result of the given lambda selection. + /// + public IEnumerable LookupAllResult(string idKey) + { + foreach (var idf in LookupAllIdent(idKey)) + { + var res = _lambdaSelectResult?.Invoke(idf); + if (res != null) + yield return res; + } + } + + /// + /// Lookup first element for id idKey and + /// return the result of the given lambda selection. + /// + public ME LookupFirstResult(string idKey) + { + return LookupAllResult(idKey).FirstOrDefault(); + } + + /// + /// Lookup first element for id idKey and + /// return the result of the given lambda selection. + /// Note: just a shortcut to LookupFirstResult() + /// + public ME Lookup(string idKey) + { + return LookupFirstResult(idKey); + } + + public IEnumerable LookupFor(IEnumerable references) + { + // access + if (references == null) + yield break; + + // translare + foreach (var inref in references) + { + yield return LookupFirstResult(inref?.Value); + } + } + } + + public class SammIdfTuple + { + public Aas.IConceptDescription CD; + public Samm.ModelElement ME; + + public SammIdfTuple() { } + + public SammIdfTuple( + Aas.IConceptDescription cd, + Samm.ModelElement me) + { + CD = cd; + ME = me; + } + } + + /// + /// Dedicated IdentifiableLookupStore for Samm.ModelElements in ConceptDescriptions. + /// + public class SammModelElementLookupStore : IdentifiableLookupStore + { + /// + /// Lookup first element for id idKey and + /// return the result of the given lambda selection. + /// Note: just a shortcut to LookupFirstResult() + /// + public SammIdfTuple Lookup(SammReference sr) + { + return LookupFirstResult(sr?.Value); + } + + /// + /// Lookup first element for id idKey and + /// return the result of the given lambda selection. + /// Note: just a shortcut to LookupFirstResult() + /// + public T Lookup(SammReference sr) where T : Samm.ModelElement + { + return LookupFirstResult(sr?.Value) as T; + } + } + + /// + /// This class provides transformation from SAMM models to other models, + /// e.g Submodel instances. + /// + public class SammTransformation + { + public Aas.IReference CreateSemanticId(string id) + { + return new Aas.Reference( + ReferenceTypes.ExternalReference, + (new Aas.IKey[] { + new Aas.Key(KeyTypes.GlobalReference, id) + }).ToList()); + } + + public void CreateSubmodelElementsInto( + Aas.IEnvironment env, + SammModelElementLookupStore store, + Samm.Aspect aspect, + List aasElems, + IEnumerable sammElems) + { + // access + if (env == null || aasElems == null || sammElems == null || aspect == null) + return; + + // iterate over SAMM properties + foreach (var osrProp in sammElems) + { + var sitProp = store.Lookup(osrProp); + var meProp = sitProp?.ME as Samm.Property; + if (meProp == null) + continue; + + // keep track for later use + Aas.ISubmodelElement addedElem = null; + + // ok, a Submodel element shall be created. + // But more details (SMC? Property?) are only avilable via + // Characteristic -> dataType .. + var sitChar = store.Lookup(meProp.Characteristic); + var meChar = sitChar?.ME as Samm.Characteristic; + + // first check the case, that the Characteristic -> dataType goes to an entity + var meDt = store.Lookup(meChar?.DataType); + if (meDt != null && meDt.ME is Samm.Entity meDtEnt) + { + // make a SMC and add directl + var newSmc = new Aas.SubmodelElementCollection( + idShort: "" + sitProp.CD.IdShort, + semanticId: CreateSemanticId(meDt.CD.Id), + value: new List()); + addedElem = newSmc; + aasElems.Add(newSmc); + + // recurse into it .. + CreateSubmodelElementsInto( + env, store, aspect, + newSmc.Value, + meDtEnt.Properties); + } + else + { + // if in doubt, create a Property with xsd:string + Aas.DataTypeDefXsd valueType = DataTypeDefXsd.String; + + // get a "handy" uri from Characteristic -> dataType + var dataTypeChar = aspect.Namespaces?.PrefixUri(meChar?.DataType?.Value); + if (dataTypeChar?.StartsWith("xsd:") == true) + { + // be a bit carefull with de-serialization + try + { + dataTypeChar = dataTypeChar.Replace("xsd:", "xs:"); + var x = Aas.Stringification.DataTypeDefXsdFromString(dataTypeChar); + if (x.HasValue) + valueType = x.Value; + } + catch (Exception ex) + { + Log.Singleton.Error(ex, $"when using XSD datatype {dataTypeChar}"); + } + } + + // make a Property + var newProp = new Aas.Property( + valueType: valueType, + semanticId: CreateSemanticId(sitProp.CD.Id), + idShort: "" + sitProp.CD?.IdShort); + addedElem = newProp; + aasElems.Add(newProp); + } + + // elaborate added element further + if (addedElem != null) + { + if (osrProp.Optional) + { + // add [0..1] + addedElem.Qualifiers = new List + { + AasPresetHelper.CreateQualifierSmtCardinality(AasPresetHelper.SmtCardinality.ZeroToOne) + }; + } + } + } + } + + public void CreateSubmodelInstanceFromAspectCD( + Aas.IEnvironment env, + Aas.IConceptDescription cdAspect) + { + // access + if (env?.ConceptDescriptions == null || cdAspect == null) + return; + + // create store + var store = new SammModelElementLookupStore(); + store.StartDictionaryAccess( + env.ConceptDescriptions, + lambdaSelectResult: (cd) => { + var me = DispEditHelperSammModules.CheckReferableForSammElements(cd).FirstOrDefault(); + return new SammIdfTuple(cd, me); + }); + + // access Aspect + if (!(store.Lookup(cdAspect.Id)?.ME is Samm.Aspect meAspect)) + { + Log.Singleton.Error("Cannot access the SAMM Aspect. Aborting!"); + return; + } + + // create Submodel + var submodel = new Aas.Submodel( + idShort: "From_SAMM_" + cdAspect.IdShort, + id: "" + AdminShellUtil.GenerateIdAccordingTemplate(Options.Curr.TemplateIdSubmodelInstance), + description: cdAspect.Description?.Copy(), + semanticId: CreateSemanticId(cdAspect.Id), + submodelElements: new List()); + + // iterate over elements + CreateSubmodelElementsInto( + env, store, meAspect, + submodel.SubmodelElements, + meAspect.Properties); + + // add Submodel + env.Add(submodel); + + // for convenience, add to first aas + var firstAas = env.AssetAdministrationShells?.FirstOrDefault(); + if (firstAas != null) + firstAas.AddSubmodelReference( + submodel.GetModelReference()); + } + } } diff --git a/src/AasxPackageLogic/ExplorerMenuFactory.cs b/src/AasxPackageLogic/ExplorerMenuFactory.cs index dd4082ad..a78f183c 100644 --- a/src/AasxPackageLogic/ExplorerMenuFactory.cs +++ b/src/AasxPackageLogic/ExplorerMenuFactory.cs @@ -311,8 +311,12 @@ public static AasxMenu CreateMainMenu() .Add("SmRef", "Return: Submodel generated", hidden: true)) .AddWpfBlazor(name: "MissingCdsFromKnown", header: "Missing ConceptDescriptions from pool of known", help: "For the selected element: checks which SME refer to missing " + - "ConceptDescriptions, which can be created from pool of known definitions.")) - .AddMenu(header: "Visualize …", attachPoint: "Visualize") + "ConceptDescriptions, which can be created from pool of known definitions.") + .AddWpfBlazor(name: "SubmodelInstanceFromSammAspect", + header: "New Submodel instance from selected SAMM aspect", + help: "Creates a new Submodel instance from an selected ConceptDescription with a SAMM Aspect element.", + args: null)) + .AddMenu(header: "Visualize …", attachPoint: "Visualize") .AddSeparator() .AddWpfBlazor(name: "ConvertElement", header: "Convert …", help: "Asks plugins if these could make offers to convert the current elements and " + diff --git a/src/AasxPackageLogic/MainWindowAnyUiDialogs.cs b/src/AasxPackageLogic/MainWindowAnyUiDialogs.cs index d2c5d08d..8d4c7457 100644 --- a/src/AasxPackageLogic/MainWindowAnyUiDialogs.cs +++ b/src/AasxPackageLogic/MainWindowAnyUiDialogs.cs @@ -1529,7 +1529,26 @@ await DisplayContextPlus.CheckIfDownloadAndStart( MainWindow.RedrawAllElementsAndFocus(); } - if (cmd == "convertelement") + if (cmd == "submodelinstancefromsammaspect") + { + // simply pass on + try + { + // delegate futher + await CommandBinding_GeneralDispatchHeadless(cmd, menuItem, ticket); + } + catch (Exception ex) + { + LogErrorToTicket(ticket, ex, + $"When executing command {cmd}, an error occurred"); + } + + // redisplay + if (ticket.Success) + MainWindow.RedrawAllElementsAndFocus(); + } + + if (cmd == "convertelement") { // check var rf = ticket.DereferencedMainDataObject as Aas.IReferable; diff --git a/src/AasxPackageLogic/MainWindowHeadless.cs b/src/AasxPackageLogic/MainWindowHeadless.cs index 0e71f719..b3552ae4 100644 --- a/src/AasxPackageLogic/MainWindowHeadless.cs +++ b/src/AasxPackageLogic/MainWindowHeadless.cs @@ -1267,7 +1267,35 @@ record = rec; ticket.Success = true; } - if (cmd == "convertelement") + if (cmd == "submodelinstancefromsammaspect") + { + // arguments + if (ticket.Env == null + || ticket.ConceptDescription == null) + { + LogErrorToTicket(ticket, + "Create Submodel instance form SAMM aspect model: No valid AAS environment or " + + "no ConceptDescription selected available."); + return; + } + + // use utility + try + { + var trans = new SammTransformation(); + trans.CreateSubmodelInstanceFromAspectCD(ticket.Env, ticket.ConceptDescription); + } + catch (Exception ex) + { + Log.Singleton.Error( + ex, $"When creating Submodel instance from SAMM Aspect model."); + } + + Log.Singleton.Info($"Done."); + ticket.Success = true; + } + + if (cmd == "convertelement") { // arguments var rf = ticket.DereferencedMainDataObject as Aas.IReferable;