718 lines
16 KiB
C#
718 lines
16 KiB
C#
/*
|
|
|
|
Copyright (C) Arnaud Houdelette 2012-2014
|
|
Copyright (C) Emmanuel Langlois 2012-2014
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 2 of the License.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Xml;
|
|
using System.Collections.ObjectModel;
|
|
using System.Threading;
|
|
|
|
namespace DMX2
|
|
{
|
|
public class SequenceurMacro : Sequenceur
|
|
{
|
|
public class Ligne {
|
|
public Ligne(){}
|
|
string nom = string.Empty;
|
|
TimeSpan top = TimeSpan.MinValue;
|
|
string circuits = string.Empty;
|
|
int valeur;
|
|
TimeSpan temps = TimeSpan.Zero;
|
|
|
|
public string Nom {
|
|
get {
|
|
return nom;
|
|
}
|
|
set {
|
|
nom = value;
|
|
}
|
|
}
|
|
|
|
public TimeSpan Top {
|
|
get {
|
|
return top;
|
|
}
|
|
set {
|
|
top = value;
|
|
}
|
|
}
|
|
|
|
public string Circuits {
|
|
get {
|
|
return circuits;
|
|
}
|
|
set {
|
|
circuits = value;
|
|
}
|
|
}
|
|
|
|
public int Valeur {
|
|
get {
|
|
return valeur;
|
|
}
|
|
set {
|
|
valeur = value;
|
|
}
|
|
}
|
|
|
|
public TimeSpan Temps {
|
|
get {
|
|
return temps;
|
|
}
|
|
set {
|
|
temps = value;
|
|
}
|
|
}
|
|
|
|
|
|
public void Save (XmlElement parent)
|
|
{
|
|
XmlElement el = parent.OwnerDocument.CreateElement ("Ligne");
|
|
parent.AppendChild (el);
|
|
|
|
el.SetAttribute ("nom", nom);
|
|
el.SetAttribute ("top", top.ToString ());
|
|
el.SetAttribute ("circuits", circuits);
|
|
el.SetAttribute ("valeur", valeur.ToString ());
|
|
el.SetAttribute ("temps", temps.ToString ());
|
|
|
|
|
|
}
|
|
|
|
public static Ligne Load (Conduite c, XmlElement el)
|
|
{
|
|
Ligne l = new Ligne();
|
|
l.nom = el.GetAttribute ("nom");
|
|
l.top = TimeSpan.Parse(el.GetAttribute("top"));
|
|
l.circuits = el.GetAttribute ("circuits");
|
|
l.valeur = int.Parse(el.GetAttribute("valeur"));
|
|
l.temps = TimeSpan.Parse(el.GetAttribute("temps"));
|
|
|
|
return l;
|
|
}
|
|
}
|
|
|
|
public class EffetMacro
|
|
{
|
|
public EffetMacro( TimeSpan tempsTransition, int valeurInitiale, int valeurFinale) {
|
|
TempsPasse = TimeSpan.Zero;
|
|
TempsTransition = tempsTransition;
|
|
ValeurInitiale= valeurInitiale;
|
|
ValeurFinale = valeurFinale;
|
|
}
|
|
public TimeSpan TempsPasse { get; set; }
|
|
public TimeSpan TempsTransition { get; set; }
|
|
public int ValeurInitiale { get; set; }
|
|
public int ValeurFinale { get; set; }
|
|
public bool Incremente (TimeSpan delta)
|
|
{
|
|
TempsPasse += delta;
|
|
if (TempsPasse > TempsTransition) {
|
|
TempsPasse = TempsTransition;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
public int ValeurCourante()
|
|
{
|
|
double progression = TempsPasse.TotalMilliseconds/TempsTransition.TotalMilliseconds;
|
|
return (int)( progression * (ValeurFinale - ValeurInitiale) + ValeurInitiale );
|
|
}
|
|
}
|
|
|
|
List<Ligne> lignes = new List<Ligne>();
|
|
|
|
Ligne aSuivre = null;
|
|
Ligne enCours = null;
|
|
Ligne ligneMaitre = null;
|
|
|
|
TimeSpan timeStamp = TimeSpan.Zero;
|
|
TimeSpan topSuivant = TimeSpan.Zero;
|
|
bool topPresent = false;
|
|
|
|
List<Circuit> circuitsSeq = new List<Circuit> ();
|
|
|
|
|
|
Dictionary<Circuit,int> valeurscourantes = new Dictionary<Circuit, int> ();
|
|
Dictionary<Circuit,EffetMacro> effetsEnCours = new Dictionary<Circuit, EffetMacro>();
|
|
|
|
actionEventTargetEx masterEventTarget=null;
|
|
actionEventTarget goNextEventTarget=null;
|
|
actionEventTarget goBackEventTarget=null;
|
|
|
|
|
|
SeqMacroUI ui = null;
|
|
bool change = false;
|
|
int master = 100;
|
|
|
|
public int Master {
|
|
get {
|
|
return master;
|
|
}
|
|
set {
|
|
master = value;
|
|
masterEventTarget.FeedBack ();
|
|
}
|
|
}
|
|
|
|
bool paused=false;
|
|
|
|
public bool Paused {
|
|
get {
|
|
return paused;
|
|
}
|
|
set {
|
|
paused = value;
|
|
}
|
|
}
|
|
|
|
public SequenceurMacro ()
|
|
{
|
|
masterEventTarget = new actionEventTargetEx (
|
|
delegate(EventData data) {
|
|
Master = 100 * data.value / 255;
|
|
return true;
|
|
},
|
|
delegate{
|
|
return Master *255/100;
|
|
},
|
|
true
|
|
);
|
|
|
|
goNextEventTarget = new actionEventTarget (
|
|
delegate(EventData data) {
|
|
if(data.value==255) LigneSuivante();
|
|
return true;
|
|
}
|
|
);
|
|
|
|
goBackEventTarget = new actionEventTarget (
|
|
delegate(EventData data) {
|
|
if(data.value==255){
|
|
if (IndexLigneEnCours > 0) {
|
|
IndexLigneaSuivre = IndexLigneEnCours - 1;
|
|
LigneSuivante ();
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
public int IndexLigneEnCours
|
|
{
|
|
get {
|
|
if (enCours == null) return -1;
|
|
return lignes.IndexOf(enCours);
|
|
}
|
|
}
|
|
|
|
|
|
public int IndexLigneaSuivre
|
|
{
|
|
get {
|
|
if (aSuivre == null)
|
|
return -1;
|
|
return lignes.IndexOf (aSuivre);
|
|
}
|
|
set {
|
|
aSuivre = lignes[value];
|
|
}
|
|
}
|
|
|
|
public int AjoutLigne (int pos)
|
|
{
|
|
lock (this) {
|
|
lignes.Insert (pos, new Ligne ());
|
|
CommandAdd(pos);
|
|
return pos;
|
|
}
|
|
}
|
|
|
|
public void RetireLigne (int pos)
|
|
{
|
|
lock (this) {
|
|
if (lignes [pos] == enCours) {
|
|
enCours = null;
|
|
if (pos + 1 < lignes.Count)
|
|
aSuivre = lignes [pos + 1];
|
|
}
|
|
if (lignes [pos] == aSuivre)
|
|
aSuivre = null;
|
|
lignes.RemoveAt (pos);
|
|
CommandRemove(pos);
|
|
}
|
|
}
|
|
|
|
public TimeSpan TimeStamp {
|
|
get {
|
|
return timeStamp;
|
|
}
|
|
}
|
|
|
|
|
|
public ReadOnlyCollection<Ligne> Lignes {
|
|
get {
|
|
return lignes.AsReadOnly();
|
|
}
|
|
}
|
|
|
|
|
|
public ReadOnlyCollection<Circuit> Circuits {
|
|
get {
|
|
return circuitsSeq.AsReadOnly ();
|
|
}
|
|
}
|
|
|
|
public void ChangeCircuits (System.Collections.Generic.List<Circuit> list)
|
|
{
|
|
foreach (var c in circuitsSeq.ToArray()) {
|
|
if (!list.Contains (c))
|
|
lock (this) RetireCircuit (c);
|
|
}
|
|
foreach (var c in list)
|
|
if (!circuitsSeq.Contains (c))
|
|
lock (this) AjouteCircuit (c);
|
|
circuitsSeq = list;
|
|
}
|
|
|
|
void AjouteCircuit (Circuit c)
|
|
{
|
|
valeurscourantes [c] = 0;
|
|
}
|
|
|
|
void RetireCircuit (Circuit c)
|
|
{
|
|
circuitsSeq.Remove (c);
|
|
valeurscourantes.Remove (c);
|
|
}
|
|
|
|
/*public override void MajCircuitsSupprimes ()
|
|
{
|
|
lock (this) {
|
|
foreach (var c in circuitsSeq.ToArray()) {
|
|
if (!Conduite.Courante.Circuits.Contains (c))
|
|
RetireCircuit (c);
|
|
}
|
|
}
|
|
}*/
|
|
|
|
public override int ValeurCircuit (Circuit c)
|
|
{
|
|
if (!valeurscourantes.ContainsKey (c))
|
|
return 0;
|
|
if(master !=100)
|
|
return valeurscourantes [c] * master /100;
|
|
return valeurscourantes [c];
|
|
}
|
|
|
|
public int ValeurBruteCircuit (Circuit c)
|
|
{
|
|
if (!circuitsSeq.Contains (c))
|
|
return 0;
|
|
return valeurscourantes [c];
|
|
}
|
|
|
|
public bool EstActif (Circuit c)
|
|
{
|
|
lock (this)
|
|
return effetsEnCours.ContainsKey(c);
|
|
}
|
|
|
|
public override void Tick (TimeSpan time)
|
|
{
|
|
if (paused)
|
|
return;
|
|
timeStamp += time;
|
|
|
|
if (Monitor.TryEnter (this)) {
|
|
try {
|
|
if (effetsEnCours.Count > 0) {
|
|
List<Circuit> circuits = new List<Circuit> (effetsEnCours.Keys);
|
|
|
|
foreach (Circuit c in circuits) {
|
|
if (effetsEnCours [c].Incremente (time)) {
|
|
valeurscourantes [c] = effetsEnCours [c].ValeurFinale;
|
|
effetsEnCours.Remove (c);
|
|
} else {
|
|
valeurscourantes [c] = effetsEnCours [c].ValeurCourante ();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (topPresent) {
|
|
if (timeStamp > topSuivant) {
|
|
LigneSuivante ();
|
|
}
|
|
}
|
|
} finally {
|
|
Monitor.Exit (this);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void LigneSuivante ()
|
|
{
|
|
lock (this) {
|
|
if(lignes.Count==0) return;
|
|
int index;
|
|
change = true; topPresent = false;
|
|
if(aSuivre==null) // selection souris
|
|
{
|
|
index = IndexLigneEnCours +1; // Premier effet si aucun précédement
|
|
if(index>= lignes.Count) index = 0; // Boucle si arrivé à la fin
|
|
|
|
enCours = lignes[index];
|
|
|
|
// Gestion de la Reprise
|
|
if(enCours.Circuits.ToLower().Equals("r") && ligneMaitre != null)
|
|
enCours = ligneMaitre;
|
|
|
|
if(enCours.Nom.Length!=0)
|
|
{
|
|
ligneMaitre = enCours;
|
|
timeStamp = TimeSpan.Zero;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
enCours = aSuivre;
|
|
ligneMaitre = enCours;
|
|
timeStamp = TimeSpan.Zero;
|
|
|
|
}
|
|
index = IndexLigneEnCours+1;
|
|
if(index<lignes.Count)
|
|
{
|
|
// Si top présent - non negatif
|
|
if(lignes[index].Top>= TimeSpan.Zero)
|
|
{
|
|
topPresent = true;
|
|
topSuivant= lignes[index].Top;
|
|
}
|
|
}
|
|
aSuivre = null;
|
|
LanceEffetsMacro(false,TimeSpan.Zero);
|
|
if(ui!=null)
|
|
ui.EffetChange();
|
|
}
|
|
}
|
|
|
|
void LanceEffetsMacro (bool ecrase, TimeSpan tempsMatrice)
|
|
{
|
|
lock (this) {
|
|
if (ecrase)
|
|
LanceEffetsMacro (enCours.Circuits, enCours.Valeur, tempsMatrice);
|
|
else
|
|
LanceEffetsMacro (enCours.Circuits, enCours.Valeur, enCours.Temps);
|
|
}
|
|
|
|
}
|
|
|
|
void LanceEffetsMacro (string circuits, int valeurCible, TimeSpan temps)
|
|
{
|
|
if(circuits==null)return;
|
|
string[] blocs = circuits.Split(',');
|
|
foreach (string bloc in blocs) {
|
|
string[] circ = bloc.Split('-');
|
|
int start, end;
|
|
if(! int.TryParse(circ[0], out start))
|
|
continue;
|
|
if(circ.Length > 1) // on a affaire a un bloc (de plusieurs)
|
|
{
|
|
if(! int.TryParse(circ[1], out end))
|
|
continue;
|
|
}
|
|
else
|
|
end= start;
|
|
for( int i = start; i<=end; i++)
|
|
{
|
|
Circuit c = Conduite.Courante.GetCircuitByID(i);
|
|
if(circuitsSeq.Contains(c))
|
|
{
|
|
lock(this)
|
|
effetsEnCours[c] = new EffetMacro(temps,valeurscourantes[c],valeurCible);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
public bool LigneChange ()
|
|
{
|
|
if (change) {
|
|
change = false;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public override void Save (System.Xml.XmlElement parent)
|
|
{
|
|
System.Xml.XmlElement el = parent.OwnerDocument.CreateElement ("SequenceurMacro");
|
|
System.Xml.XmlElement xmlEl;
|
|
|
|
parent.AppendChild (el);
|
|
el.SetAttribute ("id", ID.ToString ());
|
|
el.SetAttribute ("name", Name);
|
|
//el.SetAttribute ("master", master.ToString ());
|
|
|
|
el.AppendChild(xmlEl = parent.OwnerDocument.CreateElement ("Master"));
|
|
xmlEl.SetAttribute("value",master.ToString());
|
|
Conduite.Courante.EventManager.SaveBindings(xmlEl,masterEventTarget);
|
|
|
|
xmlEl = parent.OwnerDocument.CreateElement ("EffetSuivant");
|
|
if(Conduite.Courante.EventManager.SaveBindings(xmlEl,goNextEventTarget )) el.AppendChild(xmlEl);
|
|
|
|
xmlEl = parent.OwnerDocument.CreateElement ("EffetPrecedent");
|
|
if(Conduite.Courante.EventManager.SaveBindings(xmlEl,goBackEventTarget )) el.AppendChild(xmlEl);
|
|
|
|
foreach (Circuit c in circuitsSeq) {
|
|
el.AppendChild(xmlEl = parent.OwnerDocument.CreateElement ("CircuitSeq"));
|
|
xmlEl.SetAttribute("id",c.ID.ToString());
|
|
}
|
|
|
|
foreach (Ligne li in lignes) {
|
|
li.Save(el);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
public override SequenceurUI GetUI ()
|
|
{
|
|
|
|
if (ui == null) {
|
|
ui = new SeqMacroUI (this);
|
|
ui.Destroyed += UiDestroyed;
|
|
}
|
|
|
|
return ui;
|
|
}
|
|
|
|
void UiDestroyed (object sender, EventArgs e)
|
|
{
|
|
ui = null;
|
|
}
|
|
|
|
public void BindMasterEvent (string eventId)
|
|
{
|
|
if(eventId.Length==0)
|
|
{
|
|
Conduite.Courante.EventManager.Unbind(masterEventTarget);
|
|
return;
|
|
}
|
|
Conduite.Courante.EventManager.Bind(eventId,masterEventTarget);
|
|
}
|
|
|
|
public void BindEffetSuivantEvent (string eventId)
|
|
{
|
|
if (eventId.Length == 0) {
|
|
Conduite.Courante.EventManager.Unbind (goNextEventTarget);
|
|
return;
|
|
}
|
|
Conduite.Courante.EventManager.Bind(eventId,goNextEventTarget);
|
|
}
|
|
|
|
public void BindEffetPrecedentEvent (string eventId)
|
|
{
|
|
if (eventId.Length == 0) {
|
|
Conduite.Courante.EventManager.Unbind (goBackEventTarget);
|
|
return;
|
|
}
|
|
Conduite.Courante.EventManager.Bind(eventId,goBackEventTarget);
|
|
}
|
|
|
|
|
|
public static new SequenceurMacro Load (Conduite conduite, System.Xml.XmlElement el)
|
|
{
|
|
SequenceurMacro seq = new SequenceurMacro();
|
|
seq.LoadSeq(conduite,el);
|
|
return seq;
|
|
}
|
|
|
|
private void LoadSeq (Conduite conduite, System.Xml.XmlElement el)
|
|
{
|
|
ID = int.Parse (el.GetAttribute ("id"));
|
|
Name = el.GetAttribute ("name");
|
|
|
|
XmlElement xmlE;
|
|
|
|
if ((xmlE = el["Master"]) != null) {
|
|
master = int.Parse (xmlE.TryGetAttribute("value","100"));
|
|
foreach(string id in EventManager.LoadBindings(xmlE))
|
|
BindMasterEvent(id);
|
|
}
|
|
else master = int.Parse (el.TryGetAttribute("master","100"));
|
|
|
|
if ((xmlE = el["EffetSuivant"])!= null)
|
|
foreach(string id in EventManager.LoadBindings(xmlE))
|
|
BindEffetSuivantEvent(id);
|
|
|
|
if ((xmlE = el["EffetPrecedent"])!= null)
|
|
foreach(string id in EventManager.LoadBindings(xmlE))
|
|
BindEffetPrecedentEvent(id);
|
|
|
|
foreach (var xc in el.GetElementsByTagName("CircuitSeq")) {
|
|
System.Xml.XmlElement xcir = xc as System.Xml.XmlElement;
|
|
Circuit c = conduite.GetCircuitByID (int.Parse (xcir.GetAttribute ("id")));
|
|
circuitsSeq.Add (c);
|
|
AjouteCircuit (c);
|
|
}
|
|
|
|
foreach (var xe in el.GetElementsByTagName("Ligne"))
|
|
lignes.Add(Ligne.Load(conduite,xe as System.Xml.XmlElement));
|
|
}
|
|
|
|
static System.Text.RegularExpressions.Regex regexCommandeDirecte = new System.Text.RegularExpressions.Regex(
|
|
@"(?<circuits>[\d,-]+) (?<valeur>\d+) (?<temps>\d+)",
|
|
System.Text.RegularExpressions.RegexOptions.Compiled);
|
|
|
|
|
|
public void CommandDirecte (string text)
|
|
{
|
|
var cmd = regexCommandeDirecte.Match (text);
|
|
if (cmd.Success) {
|
|
string circuits = cmd.Groups["circuits"].Value;
|
|
|
|
int valeur = int.Parse(cmd.Groups["valeur"].Value);
|
|
TimeSpan temps = TimeSpan.FromMilliseconds(100* int.Parse(cmd.Groups["temps"].Value));
|
|
|
|
LanceEffetsMacro(circuits,valeur,temps);
|
|
}
|
|
}
|
|
|
|
static System.Text.RegularExpressions.Regex regexCommand1 = new System.Text.RegularExpressions.Regex(
|
|
@"(?<effet>\d+)(t(?<transition>\d+))?",
|
|
System.Text.RegularExpressions.RegexOptions.Compiled);
|
|
|
|
static System.Text.RegularExpressions.Regex regexCommand2 = new System.Text.RegularExpressions.Regex(
|
|
@"(?<effet>\d+)(?<params>(t\d+)?)?",
|
|
System.Text.RegularExpressions.RegexOptions.Compiled);
|
|
|
|
|
|
|
|
public override void Command (string command)
|
|
{
|
|
lock (this) {
|
|
var cmd = regexCommand1.Match(command);
|
|
|
|
if (cmd.Success) {
|
|
if (cmd.Groups ["effet"].Success) {
|
|
int effet = int.Parse (cmd.Groups ["effet"].Value) - 1;
|
|
int transition=-1;
|
|
|
|
if(effet>=lignes.Count) return;
|
|
enCours = lignes[effet];
|
|
ligneMaitre = enCours;
|
|
timeStamp = TimeSpan.Zero;
|
|
|
|
topPresent = false;
|
|
int index = IndexLigneEnCours+1;
|
|
if(index<lignes.Count)
|
|
{
|
|
// Si top présent - non negatif
|
|
if(lignes[index].Top>= TimeSpan.Zero)
|
|
{
|
|
topPresent = true;
|
|
topSuivant= lignes[index].Top;
|
|
}
|
|
}
|
|
aSuivre = null;
|
|
|
|
if (cmd.Groups ["transition"].Success) {
|
|
transition = int.Parse (cmd.Groups ["transition"].Value);
|
|
LanceEffetsMacro(true, TimeSpan.FromMilliseconds(transition *100));
|
|
}
|
|
else
|
|
LanceEffetsMacro(false,TimeSpan.Zero);
|
|
|
|
if(ui!=null) ui.EffetChange();
|
|
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
void CommandAdd (int index)
|
|
{
|
|
lock (Conduite.Courante.SequenceurMaitre) {
|
|
string[] commands = Conduite.Courante.SequenceurMaitre.GetCommands (this);
|
|
|
|
for (int i = 0; i < commands.Length; i++) {
|
|
var cmd = regexCommand2.Match(commands[i]);
|
|
if(cmd.Success){
|
|
int ef = int.Parse(cmd.Groups["effet"].Value);
|
|
if (ef-1>index) {
|
|
ef++;
|
|
commands[i] = ef.ToString() + cmd.Groups["params"].Value;
|
|
}
|
|
}
|
|
}
|
|
Conduite.Courante.SequenceurMaitre.SetCommands(this,commands);
|
|
}
|
|
}
|
|
|
|
void CommandRemove (int index)
|
|
{
|
|
lock (Conduite.Courante.SequenceurMaitre) {
|
|
string[] commands = Conduite.Courante.SequenceurMaitre.GetCommands (this);
|
|
|
|
for (int i = 0; i < commands.Length; i++) {
|
|
var cmd = regexCommand2.Match(commands[i]);
|
|
if(cmd.Success){
|
|
int ef = int.Parse(cmd.Groups["effet"].Value);
|
|
if (ef-1 == index)
|
|
commands[i] = string.Empty;
|
|
else if (ef-1>index) {
|
|
ef--;
|
|
commands[i] = ef.ToString() + cmd.Groups["params"].Value;
|
|
}
|
|
}
|
|
}
|
|
Conduite.Courante.SequenceurMaitre.SetCommands(this,commands);
|
|
}
|
|
}
|
|
|
|
void CommandSwap (int index)
|
|
{
|
|
lock (Conduite.Courante.SequenceurMaitre) {
|
|
string[] commands = Conduite.Courante.SequenceurMaitre.GetCommands (this);
|
|
|
|
// numeros a swapper
|
|
int a = index+1;
|
|
int b = index+2;
|
|
|
|
for (int i = 0; i < commands.Length; i++) {
|
|
var cmd = regexCommand2.Match(commands[i]);
|
|
if(cmd.Success){
|
|
int ef = int.Parse(cmd.Groups["effet"].Value);
|
|
if (ef == a)
|
|
commands[i] = b.ToString() + cmd.Groups["params"].Value;
|
|
if (ef == b)
|
|
commands[i] = a.ToString() + cmd.Groups["params"].Value;
|
|
}
|
|
}
|
|
Conduite.Courante.SequenceurMaitre.SetCommands(this,commands);
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|