@@ -14,6 +14,25 @@ import {
1414 identity ,
1515} from '@openenergytools/scl-lib' ;
1616import '@openenergytools/filterable-lists/dist/action-list.js' ;
17+ import {
18+ isFCDACompatibleWithIED ,
19+ queryLDevice ,
20+ queryLN ,
21+ } from '../foundation/utils/xml.js' ;
22+
23+ // eslint-disable-next-line no-shadow
24+ export enum ControlBlockCopyStatus {
25+ CanCopy = 'CanCopy' ,
26+ IEDStructureIncompatible = 'IEDStructureIncompatible' ,
27+ ControlBlockOrDataSetAlreadyExists = 'ControlBlockOrDataSetAlreadyExists' ,
28+ }
29+
30+ export interface ControlBlockCopyOption {
31+ ied : Element ;
32+ control : Element ;
33+ status : ControlBlockCopyStatus ;
34+ selected : boolean ;
35+ }
1736
1837export class BaseElementEditor extends ScopedElementsMixin ( LitElement ) {
1938 /** The document being edited as provided to plugins by [[`OpenSCD`]]. */
@@ -30,12 +49,21 @@ export class BaseElementEditor extends ScopedElementsMixin(LitElement) {
3049 @state ( )
3150 selectedDataSet ?: Element | null ;
3251
52+ @state ( )
53+ controlBlockCopyOptions : ControlBlockCopyOption [ ] = [ ] ;
54+
3355 @query ( '.dialog.select' ) selectDataSetDialog ! : MdDialog ;
3456
57+ @query ( '.dialog.copy' ) copyControlBlockDialog ! : MdDialog ;
58+
3559 @query ( '.new.dataset' ) newDataSet ! : MdIconButton ;
3660
3761 @query ( '.change.dataset' ) changeDataSet ! : MdIconButton ;
3862
63+ get hasCopyControlSelected ( ) : boolean {
64+ return this . controlBlockCopyOptions . some ( o => o . selected ) ;
65+ }
66+
3967 protected selectDataSet ( dataSet : Element ) : void {
4068 const name = dataSet . getAttribute ( 'name' ) ;
4169 if ( ! name || ! this . selectCtrlBlock ) return ;
@@ -54,6 +82,130 @@ export class BaseElementEditor extends ScopedElementsMixin(LitElement) {
5482 this . selectDataSetDialog . close ( ) ;
5583 }
5684
85+ protected queryIEDs ( ) : Element [ ] {
86+ return Array . from ( this . doc . querySelectorAll ( ':root > IED' ) ) ;
87+ }
88+
89+ // eslint-disable-next-line class-methods-use-this
90+ protected queryLnForControl ( ied : Element , control : Element ) : Element | null {
91+ const lDevice = control . closest ( 'LDevice' ) ;
92+ const lnOrLn0 = control . closest ( 'LN0, LN' ) ;
93+
94+ if ( ! lnOrLn0 || ! lDevice ) {
95+ throw new Error ( 'ControlBlock must be a child of LN or LN0 and LDevice' ) ;
96+ }
97+
98+ const ldInst = lDevice . getAttribute ( 'inst' ) ?? '' ;
99+ const lDeviceInIed = queryLDevice ( ied , ldInst ) ;
100+
101+ if ( ! lDeviceInIed ) {
102+ return null ;
103+ }
104+
105+ const lnClass = lnOrLn0 . getAttribute ( 'lnClass' ) ?? '' ;
106+ const inst = lnOrLn0 . getAttribute ( 'inst' ) ?? '' ;
107+ const prefix = lnOrLn0 . getAttribute ( 'prefix' ) ;
108+
109+ return queryLN ( lDeviceInIed , lnClass , inst , prefix ) ;
110+ }
111+
112+ // eslint-disable-next-line class-methods-use-this
113+ protected getDataSet ( control : Element ) : Element | null {
114+ return (
115+ control . parentElement ?. querySelector (
116+ `DataSet[name="${ control . getAttribute ( 'datSet' ) } "]`
117+ ) ?? null
118+ ) ;
119+ }
120+
121+ // eslint-disable-next-line class-methods-use-this
122+ protected getCopyControlBlockCopyStatus (
123+ controlBlock : Element ,
124+ otherIED : Element
125+ ) : ControlBlockCopyStatus {
126+ const ln = this . queryLnForControl ( otherIED , controlBlock ) ;
127+
128+ if ( ! ln ) {
129+ return ControlBlockCopyStatus . IEDStructureIncompatible ;
130+ }
131+
132+ const controlInOtherIED = ln . querySelector (
133+ `${ controlBlock . tagName } [name="${ controlBlock . getAttribute ( 'name' ) } "]`
134+ ) ;
135+ const hasControl = Boolean ( controlInOtherIED ) ;
136+
137+ const dataSet = this . getDataSet ( controlBlock ) ;
138+
139+ if ( ! dataSet ) {
140+ throw new Error ( 'ControlBlock has no DataSet' ) ;
141+ }
142+
143+ const hasDataSet =
144+ dataSet !== null &&
145+ Boolean (
146+ ln . querySelector ( `DataSet[name="${ dataSet ?. getAttribute ( 'name' ) } "]` )
147+ ) ;
148+
149+ if ( hasDataSet || hasControl ) {
150+ return ControlBlockCopyStatus . ControlBlockOrDataSetAlreadyExists ;
151+ }
152+
153+ const fcdas = Array . from ( dataSet . querySelectorAll ( 'FCDA' ) ) ;
154+ for ( const fcda of fcdas ) {
155+ const isCompatible = isFCDACompatibleWithIED ( fcda , otherIED ) ;
156+
157+ if ( ! isCompatible ) {
158+ return ControlBlockCopyStatus . IEDStructureIncompatible ;
159+ }
160+ }
161+
162+ return ControlBlockCopyStatus . CanCopy ;
163+ }
164+
165+ protected copyControlBlock ( ) : void {
166+ const selectedOptions = this . controlBlockCopyOptions . filter (
167+ o => o . selected
168+ ) ;
169+
170+ if ( selectedOptions . length === 0 ) {
171+ this . copyControlBlockDialog . close ( ) ;
172+ return ;
173+ }
174+
175+ const inserts = selectedOptions . flatMap ( o => {
176+ const ln = this . queryLnForControl ( o . ied , o . control ) ;
177+ const dataSet = this . getDataSet ( o . control ) ;
178+
179+ if ( ! ln || ! dataSet ) {
180+ throw new Error ( 'ControlBlock or DataSet not found' ) ;
181+ }
182+
183+ const controlCopy = o . control . cloneNode ( true ) as Element ;
184+ const controlInsert = {
185+ parent : ln ,
186+ node : controlCopy ,
187+ reference : null ,
188+ } ;
189+
190+ const dataSetCcopy = dataSet . cloneNode ( true ) as Element ;
191+ const dataSetInsert = {
192+ parent : ln ,
193+ node : dataSetCcopy ,
194+ reference : null ,
195+ } ;
196+
197+ return [ controlInsert , dataSetInsert ] ;
198+ } ) ;
199+
200+ this . dispatchEvent (
201+ newEditEvent ( inserts , {
202+ title : `Copy control block to ${ selectedOptions . length } IEDs` ,
203+ } )
204+ ) ;
205+
206+ this . copyControlBlockDialog . close ( ) ;
207+ }
208+
57209 private addNewDataSet ( control : Element ) : void {
58210 const parent = control . parentElement ;
59211 if ( ! parent ) return ;
@@ -79,6 +231,20 @@ export class BaseElementEditor extends ScopedElementsMixin(LitElement) {
79231 this . selectDataSetDialog . show ( ) ;
80232 }
81233
234+ // eslint-disable-next-line class-methods-use-this
235+ private getCopyStatusText ( status : ControlBlockCopyStatus ) : string {
236+ switch ( status ) {
237+ case ControlBlockCopyStatus . CanCopy :
238+ return 'Copy possible' ;
239+ case ControlBlockCopyStatus . IEDStructureIncompatible :
240+ return 'IED structure incompatible' ;
241+ case ControlBlockCopyStatus . ControlBlockOrDataSetAlreadyExists :
242+ return 'Control block or data set already exists' ;
243+ default :
244+ return '' ;
245+ }
246+ }
247+
82248 protected renderSelectDataSetDialog ( ) : TemplateResult {
83249 const items = Array . from (
84250 this . selectCtrlBlock ?. parentElement ?. querySelectorAll (
@@ -97,6 +263,52 @@ export class BaseElementEditor extends ScopedElementsMixin(LitElement) {
97263 </ md-dialog > ` ;
98264 }
99265
266+ protected renderCopyControlBlockDialog ( ) : TemplateResult {
267+ return html `< md-dialog
268+ class ="dialog copy "
269+ @close =${ ( ) => {
270+ this . controlBlockCopyOptions = [ ] ;
271+ } }
272+ >
273+ < div slot ="content " class ="copy-option-list ">
274+ ${ this . controlBlockCopyOptions . map (
275+ option =>
276+ html ` < label class ="copy-optin-row ">
277+ < div class ="copy-option-description ">
278+ < div class ="copy-option-description-ied ">
279+ ${ option . ied . getAttribute ( 'name' ) }
280+ </ div >
281+ < div class ="copy-option-description-status ">
282+ ${ this . getCopyStatusText ( option . status ) }
283+ </ div >
284+ </ div >
285+ < md-checkbox
286+ ?checked =${ option . selected }
287+ @change =${ ( ) => {
288+ // eslint-disable-next-line no-param-reassign
289+ option . selected = ! option . selected ;
290+ this . requestUpdate ( ) ;
291+ } }
292+ ?disabled=${ option . status !== ControlBlockCopyStatus . CanCopy }
293+ >
294+ </ md-checkbox >
295+ </ label > `
296+ ) }
297+ < div class ="copy-button ">
298+ < md-outlined-button
299+ @click =${ ( ) => this . copyControlBlockDialog . close ( ) }
300+ > Close</ md-outlined-button
301+ >
302+ < md-outlined-button
303+ @click =${ this . copyControlBlock }
304+ ?disabled =${ ! this . hasCopyControlSelected }
305+ > Copy</ md-outlined-button
306+ >
307+ </ div >
308+ </ div >
309+ </ md-dialog > ` ;
310+ }
311+
100312 protected renderDataSetElementContainer ( ) : TemplateResult {
101313 return html `
102314 < div class ="content dataSet ">
0 commit comments