|
@@ -0,0 +1,494 @@
|
|
|
+package exampleAlgorithms;
|
|
|
+
|
|
|
+import java.awt.BorderLayout;
|
|
|
+import java.awt.Component;
|
|
|
+import java.awt.Dimension;
|
|
|
+import java.awt.FlowLayout;
|
|
|
+import java.awt.image.BufferedImage;
|
|
|
+import java.text.NumberFormat;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+import java.util.stream.Stream;
|
|
|
+
|
|
|
+import javax.swing.BorderFactory;
|
|
|
+import javax.swing.ImageIcon;
|
|
|
+import javax.swing.JButton;
|
|
|
+import javax.swing.JCheckBox;
|
|
|
+import javax.swing.JFormattedTextField;
|
|
|
+import javax.swing.JFrame;
|
|
|
+import javax.swing.JLabel;
|
|
|
+import javax.swing.JOptionPane;
|
|
|
+import javax.swing.JPanel;
|
|
|
+import javax.swing.JProgressBar;
|
|
|
+import javax.swing.JScrollPane;
|
|
|
+import javax.swing.JSplitPane;
|
|
|
+import javax.swing.JTextArea;
|
|
|
+import javax.swing.text.NumberFormatter;
|
|
|
+
|
|
|
+import api.Algorithm;
|
|
|
+import classes.AbstractCpsObject;
|
|
|
+import classes.CpsUpperNode;
|
|
|
+import classes.HolonElement;
|
|
|
+import classes.HolonObject;
|
|
|
+import classes.HolonSwitch;
|
|
|
+import ui.controller.Control;
|
|
|
+import ui.model.DecoratedGroupNode;
|
|
|
+import ui.model.DecoratedNetwork;
|
|
|
+import ui.model.DecoratedState;
|
|
|
+import ui.model.Model;
|
|
|
+
|
|
|
+public class DemoAlgo implements Algorithm {
|
|
|
+
|
|
|
+ //Parameter for Algo with default Values:
|
|
|
+ private boolean closeSwitches = true;
|
|
|
+
|
|
|
+ //Settings For GroupNode using and cancel
|
|
|
+ private boolean useGroupNode = false;
|
|
|
+ private DecoratedGroupNode dGroupNode = null;
|
|
|
+ private boolean cancel = false;
|
|
|
+
|
|
|
+
|
|
|
+ //Parameter defined by Algo
|
|
|
+ private HashMap<Integer, AccessWrapper> access;
|
|
|
+ private List<Boolean> initialState;
|
|
|
+ private List<HolonSwitch> switchList;
|
|
|
+ private List<HolonObject> objectList;
|
|
|
+
|
|
|
+ //Gui Part:
|
|
|
+ private Control control;
|
|
|
+ private JTextArea textArea;
|
|
|
+ private JPanel content = new JPanel();
|
|
|
+ //ProgressBar
|
|
|
+ private JProgressBar progressBar = new JProgressBar();
|
|
|
+ private int progressBarCount = 0;
|
|
|
+ private long startTime;
|
|
|
+ private Thread runThread;
|
|
|
+ //Windrad
|
|
|
+ private HolonObject windrad;
|
|
|
+
|
|
|
+
|
|
|
+ private int waitDurationWindradStep = 400;
|
|
|
+ private int waitDurationEnd = 1000;
|
|
|
+
|
|
|
+ int counter;
|
|
|
+
|
|
|
+
|
|
|
+ public static void main(String[] args)
|
|
|
+ {
|
|
|
+ JFrame newFrame = new JFrame("exampleWindow");
|
|
|
+ DemoAlgo instance = new DemoAlgo();
|
|
|
+ newFrame.setContentPane(instance.getAlgorithmPanel());
|
|
|
+ newFrame.pack();
|
|
|
+ newFrame.setVisible(true);
|
|
|
+ newFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
|
|
+ }
|
|
|
+ public DemoAlgo() {
|
|
|
+ content.setLayout(new BorderLayout());
|
|
|
+
|
|
|
+ textArea = new JTextArea();
|
|
|
+ textArea.setEditable(false);
|
|
|
+ JScrollPane scrollPane = new JScrollPane(textArea);
|
|
|
+ JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
|
|
|
+ createOptionPanel() , scrollPane);
|
|
|
+ splitPane.setResizeWeight(0.0);
|
|
|
+ content.add(splitPane, BorderLayout.CENTER);
|
|
|
+ content.setPreferredSize(new Dimension(800,800));
|
|
|
+ }
|
|
|
+ public JPanel createOptionPanel() {
|
|
|
+ JPanel optionPanel = new JPanel(new BorderLayout());
|
|
|
+ JScrollPane scrollPane = new JScrollPane(createParameterPanel());
|
|
|
+ scrollPane.setBorder(BorderFactory.createTitledBorder("Parameter"));
|
|
|
+ optionPanel.add(scrollPane, BorderLayout.CENTER);
|
|
|
+ optionPanel.add(createButtonPanel(), BorderLayout.PAGE_END);
|
|
|
+ return optionPanel;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Component createParameterPanel() {
|
|
|
+ JPanel parameterPanel = new JPanel(null);
|
|
|
+ parameterPanel.setPreferredSize(new Dimension(510,300));
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+// JLabel showDiagnosticsLabel = new JLabel("Set all switches closed:");
|
|
|
+// showDiagnosticsLabel.setBounds(200, 60, 170, 20);
|
|
|
+// parameterPanel.add(showDiagnosticsLabel);
|
|
|
+
|
|
|
+
|
|
|
+ JPanel borderPanel = new JPanel(null);
|
|
|
+ borderPanel.setBounds(200, 85, 185, 50);
|
|
|
+ borderPanel.setBorder(BorderFactory.createTitledBorder(""));
|
|
|
+ parameterPanel.add(borderPanel);
|
|
|
+
|
|
|
+ JLabel showGroupNodeLabel = new JLabel("Use Group Node:");
|
|
|
+ showGroupNodeLabel.setBounds(10, 1, 170, 20);
|
|
|
+ borderPanel.add(showGroupNodeLabel);
|
|
|
+
|
|
|
+ JButton selectGroupNodeButton = new JButton("Select GroupNode");
|
|
|
+ selectGroupNodeButton.setEnabled(false);
|
|
|
+ selectGroupNodeButton.setBounds(10, 25, 165, 20);
|
|
|
+ selectGroupNodeButton.addActionListener(actionEvent -> selectGroupNode());
|
|
|
+ borderPanel.add(selectGroupNodeButton);
|
|
|
+
|
|
|
+ JCheckBox useGroupNodeCheckBox = new JCheckBox();
|
|
|
+ useGroupNodeCheckBox.setSelected(false);
|
|
|
+ useGroupNodeCheckBox.setBounds(155, 1, 25, 20);
|
|
|
+ useGroupNodeCheckBox.addActionListener(actionEvent -> {
|
|
|
+ useGroupNode = useGroupNodeCheckBox.isSelected();
|
|
|
+ selectGroupNodeButton.setEnabled(useGroupNode);
|
|
|
+ });
|
|
|
+ borderPanel.add(useGroupNodeCheckBox);
|
|
|
+
|
|
|
+
|
|
|
+// JCheckBox switchesCheckBox = new JCheckBox();
|
|
|
+// switchesCheckBox.setSelected(closeSwitches);
|
|
|
+// switchesCheckBox.setBounds(370, 60, 25, 20);
|
|
|
+// switchesCheckBox.addActionListener(actionEvent -> closeSwitches = switchesCheckBox.isSelected());
|
|
|
+// parameterPanel.add(switchesCheckBox);
|
|
|
+
|
|
|
+ JButton selectRoom1Button = new JButton("Select");
|
|
|
+ selectRoom1Button.setBounds(10,300, 90, 20);
|
|
|
+ selectRoom1Button.addActionListener(actionEvent -> this.selectHolonObject());
|
|
|
+ parameterPanel.add(selectRoom1Button);
|
|
|
+ NumberFormat format = NumberFormat.getIntegerInstance();
|
|
|
+ format.setGroupingUsed(false);
|
|
|
+ format.setParseIntegerOnly(true);
|
|
|
+ NumberFormatter integerFormatter = new NumberFormatter(format);
|
|
|
+ integerFormatter.setMinimum(0);
|
|
|
+ integerFormatter.setCommitsOnValidEdit(true);
|
|
|
+ JLabel portLabel = new JLabel("between:");
|
|
|
+ portLabel.setBounds(10, 330, 70, 30);
|
|
|
+ parameterPanel.add(portLabel);
|
|
|
+ JFormattedTextField betweenTF = new JFormattedTextField(integerFormatter);
|
|
|
+ betweenTF.setText(""+waitDurationWindradStep);
|
|
|
+ betweenTF.setBounds(80 ,330, 80, 30);
|
|
|
+ betweenTF.addPropertyChangeListener(propertyChange ->{
|
|
|
+ String text = betweenTF.getValue().toString();
|
|
|
+ text = text.replaceAll("\\s", "");
|
|
|
+ waitDurationWindradStep = Integer.parseInt((text));
|
|
|
+ });
|
|
|
+ parameterPanel.add(betweenTF);
|
|
|
+ JLabel afterLabel = new JLabel("after:");
|
|
|
+ afterLabel.setBounds(10, 360, 70, 30);
|
|
|
+ parameterPanel.add(afterLabel);
|
|
|
+ JFormattedTextField afterTF = new JFormattedTextField(integerFormatter);
|
|
|
+ afterTF.setText(""+waitDurationEnd);
|
|
|
+ afterTF.setBounds(80 ,360, 80, 30);
|
|
|
+ afterTF.addPropertyChangeListener(propertyChange ->{
|
|
|
+ String text = afterTF.getValue().toString();
|
|
|
+ text = text.replaceAll("\\s", "");
|
|
|
+ waitDurationEnd = Integer.parseInt((text));
|
|
|
+ });
|
|
|
+ parameterPanel.add(afterTF);
|
|
|
+
|
|
|
+
|
|
|
+ return parameterPanel;
|
|
|
+ }
|
|
|
+ public JPanel createButtonPanel() {
|
|
|
+ JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
|
|
+ JButton cancelButton = new JButton("Cancel Run");
|
|
|
+ cancelButton.addActionListener(actionEvent -> cancel());
|
|
|
+ buttonPanel.add(cancelButton);
|
|
|
+ JButton clearButton = new JButton("Clear Console");
|
|
|
+ clearButton.addActionListener(actionEvent -> clear());
|
|
|
+ buttonPanel.add(clearButton);
|
|
|
+ JButton resetButton = new JButton("Reset");
|
|
|
+ resetButton.setToolTipText("Resets the State to before the Algorithm has runed.");
|
|
|
+ resetButton.addActionListener(actionEvent -> reset());
|
|
|
+ buttonPanel.add(resetButton);
|
|
|
+ JButton runButton = new JButton("Run");
|
|
|
+ runButton.addActionListener(actionEvent -> {
|
|
|
+ Runnable task = () -> run();
|
|
|
+ runThread = new Thread(task);
|
|
|
+ runThread.start();
|
|
|
+ });
|
|
|
+ buttonPanel.add(runButton);
|
|
|
+ return buttonPanel;
|
|
|
+ }
|
|
|
+ private void cancel() {
|
|
|
+ if(runThread.isAlive()) {
|
|
|
+ println("");
|
|
|
+ println("Cancel run.");
|
|
|
+ cancel = true;
|
|
|
+ progressBar.setValue(0);
|
|
|
+ } else {
|
|
|
+ println("Nothing to cancel.");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void run() {
|
|
|
+ cancel = false;
|
|
|
+ disableGuiInput(true);
|
|
|
+ startTimer();
|
|
|
+ executeDemoAlgo();
|
|
|
+ if(cancel) {
|
|
|
+ reset();
|
|
|
+ disableGuiInput(false);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ printElapsedTime();
|
|
|
+ disableGuiInput(false);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void reset() {
|
|
|
+ if(initialState != null) {
|
|
|
+ println("Resetting..");
|
|
|
+ resetState();
|
|
|
+ updateVisual();
|
|
|
+ }else {
|
|
|
+ println("No run inistialized.");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ private void disableGuiInput(boolean bool) {
|
|
|
+ control.guiDiable(bool);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public JPanel getAlgorithmPanel() {
|
|
|
+ return content;
|
|
|
+ }
|
|
|
+ @Override
|
|
|
+ public void setController(Control control) {
|
|
|
+ this.control = control;
|
|
|
+
|
|
|
+ }
|
|
|
+ private void clear() {
|
|
|
+ textArea.setText("");
|
|
|
+ }
|
|
|
+ private void print(String message) {
|
|
|
+ textArea.append(message);
|
|
|
+ }
|
|
|
+ private void println(String message) {
|
|
|
+ textArea.append(message + "\n");
|
|
|
+ }
|
|
|
+ private void selectGroupNode() {
|
|
|
+ Object[] possibilities = control.getSimManager().getActualVisualRepresentationalState().getCreatedGroupNodes().values().stream().map(aCps -> new Handle<DecoratedGroupNode>(aCps)).toArray();
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ Handle<DecoratedGroupNode> selected = (Handle<DecoratedGroupNode>) JOptionPane.showInputDialog(content, "Select GroupNode:", "GroupNode?", JOptionPane.OK_OPTION,new ImageIcon(new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB)) , possibilities, "");
|
|
|
+ if(selected != null) {
|
|
|
+ println("Selected: " + selected);
|
|
|
+ dGroupNode = selected.object;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ private void progressBarStep(){
|
|
|
+ progressBar.setValue(++progressBarCount);
|
|
|
+ }
|
|
|
+ private void calculateProgressBarParameter() {
|
|
|
+ int max = 100;
|
|
|
+ progressBarCount = 0;
|
|
|
+ progressBar.setValue(0);
|
|
|
+ progressBar.setMaximum(max);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void startTimer(){
|
|
|
+ startTime = System.currentTimeMillis();
|
|
|
+ }
|
|
|
+ private void printElapsedTime(){
|
|
|
+ long elapsedMilliSeconds = System.currentTimeMillis() - startTime;
|
|
|
+ println("Execution Time of Algo in Milliseconds:" + elapsedMilliSeconds);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ //Algo Part:
|
|
|
+
|
|
|
+ private void executeDemoAlgo() {
|
|
|
+ extractPositionAndAccess();
|
|
|
+ counter = 0;
|
|
|
+ int actualIteration = control.getModel().getCurIteration();
|
|
|
+ deactivateWindrad();
|
|
|
+ setAllSwitchesClosed();
|
|
|
+ updateVisual();
|
|
|
+
|
|
|
+ try {
|
|
|
+ //Schalte Slow das Windrad Ein
|
|
|
+ if(windrad == null)return;
|
|
|
+ for(int i = 0; i< windrad.getNumberOfElements(); i++) {
|
|
|
+ windrad.getElements().get(i).setActive(true);
|
|
|
+ TimeUnit.MILLISECONDS.sleep(waitDurationWindradStep);
|
|
|
+ updateVisual();
|
|
|
+ }
|
|
|
+ TimeUnit.MILLISECONDS.sleep(waitDurationEnd);
|
|
|
+ } catch (InterruptedException e) {
|
|
|
+ }
|
|
|
+
|
|
|
+ setHolonElemntsAktiv(actualIteration);
|
|
|
+ println("Changed Elements: " + counter);
|
|
|
+ updateVisual();
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ private void deactivateWindrad() {
|
|
|
+ if(windrad == null)return;
|
|
|
+ windrad.getElements().stream().forEach(ele -> ele.setActive(false));
|
|
|
+ }
|
|
|
+ private void setHolonElemntsAktiv(int actualIteration) {
|
|
|
+ for(int i = 0;i<access.size();i++) {
|
|
|
+ AccessWrapper aw = access.get(i);
|
|
|
+ if(aw.getState(actualIteration) ==false) counter++;
|
|
|
+ if(aw.getType() == AccessWrapper.HOLONELEMENT) aw.setState(true);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ private void setAllSwitchesClosed() {
|
|
|
+ for(HolonSwitch hSwitch : switchList) {
|
|
|
+ if(hSwitch.getManualMode() == false) counter++;
|
|
|
+ hSwitch.setManualMode(true);
|
|
|
+ hSwitch.setManualState(true);
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * Method to get the current Position alias a ListOf Booleans for aktive settings on the Objects on the Canvas.
|
|
|
+ * Also initialize the Access Hashmap to swap faster positions.
|
|
|
+ * @param model
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ private List<Boolean> extractPositionAndAccess() {
|
|
|
+ Model model = control.getModel();
|
|
|
+ switchList = new ArrayList<HolonSwitch>();
|
|
|
+ objectList = new ArrayList<HolonObject>();
|
|
|
+ initialState = new ArrayList<Boolean>();
|
|
|
+ access= new HashMap<Integer, AccessWrapper>();
|
|
|
+ rollOutNodes((useGroupNode && (dGroupNode != null))? dGroupNode.getModel().getNodes() :model.getObjectsOnCanvas(), initialState, model.getCurIteration());
|
|
|
+ return initialState;
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * Method to extract the Informations recursively out of the Model.
|
|
|
+ * @param nodes
|
|
|
+ * @param positionToInit
|
|
|
+ * @param timeStep
|
|
|
+ */
|
|
|
+ private void rollOutNodes(List<AbstractCpsObject> nodes, List<Boolean> positionToInit, int timeStep) {
|
|
|
+ for(AbstractCpsObject aCps : nodes) {
|
|
|
+ if (aCps instanceof HolonObject) {
|
|
|
+ for (HolonElement hE : ((HolonObject) aCps).getElements()) {
|
|
|
+ positionToInit.add(hE.isActive());
|
|
|
+ access.put(positionToInit.size() - 1 , new AccessWrapper(hE));
|
|
|
+ }
|
|
|
+ objectList.add((HolonObject) aCps);
|
|
|
+ }
|
|
|
+ else if (aCps instanceof HolonSwitch) {
|
|
|
+ HolonSwitch sw = (HolonSwitch) aCps;
|
|
|
+ positionToInit.add(sw.getState(timeStep));
|
|
|
+ switchList.add(sw);
|
|
|
+ access.put(positionToInit.size() - 1 , new AccessWrapper(sw));
|
|
|
+ }
|
|
|
+ else if(aCps instanceof CpsUpperNode) {
|
|
|
+ rollOutNodes(((CpsUpperNode)aCps).getNodes(), positionToInit ,timeStep );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * To let the User See the current state without touching the Canvas.
|
|
|
+ */
|
|
|
+ private void updateVisual() {
|
|
|
+ control.calculateStateAndVisualForCurrentTimeStep();
|
|
|
+ control.updateCanvas();
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * Sets the Model back to its original State before the LAST run.
|
|
|
+ */
|
|
|
+ private void resetState() {
|
|
|
+ setState(initialState);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Sets the State out of the given position for calculation or to show the user.
|
|
|
+ * @param position
|
|
|
+ */
|
|
|
+ private void setState(List<Boolean> position) {
|
|
|
+ for(int i = 0;i<position.size();i++) {
|
|
|
+ access.get(i).setState(position.get(i));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ private void selectHolonObject() {
|
|
|
+ List<HolonObject> holonObjectList = new ArrayList<HolonObject>();
|
|
|
+ addObjectToList(control.getModel().getObjectsOnCanvas(),holonObjectList);
|
|
|
+ Object[] possibilities = holonObjectList.stream().map(aCps -> new Handle<HolonObject>(aCps)).toArray();
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ Handle<HolonObject> selected = (Handle<HolonObject>) JOptionPane.showInputDialog(content, "Select HolonObject:", "HolonObject?", JOptionPane.OK_OPTION,new ImageIcon(new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB)) , possibilities, "");
|
|
|
+ if(selected != null) {
|
|
|
+ //println("Selected: " + selected);
|
|
|
+ windrad = selected.object;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ private void addObjectToList(List<AbstractCpsObject> listToSearch, List<HolonObject> listToAdd){
|
|
|
+ for (AbstractCpsObject aCps : listToSearch) {
|
|
|
+ if (aCps instanceof HolonObject) listToAdd.add((HolonObject) aCps);
|
|
|
+ else if(aCps instanceof CpsUpperNode) {
|
|
|
+ addObjectToList(((CpsUpperNode)aCps).getNodes(),listToAdd);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A Wrapper Class for Access HolonElement and HolonSwitch in one Element and not have to split the List.
|
|
|
+ */
|
|
|
+ private class AccessWrapper {
|
|
|
+ public static final int HOLONELEMENT = 0;
|
|
|
+ public static final int SWITCH = 1;
|
|
|
+ private int type;
|
|
|
+ private HolonSwitch hSwitch;
|
|
|
+ private HolonElement hElement;
|
|
|
+ public AccessWrapper(HolonSwitch hSwitch){
|
|
|
+ type = SWITCH;
|
|
|
+ this.hSwitch = hSwitch;
|
|
|
+ }
|
|
|
+ public AccessWrapper(HolonElement hElement){
|
|
|
+ type = HOLONELEMENT;
|
|
|
+ this.hElement = hElement;
|
|
|
+ }
|
|
|
+ public void setState(boolean state) {
|
|
|
+ if(type == HOLONELEMENT) {
|
|
|
+ hElement.setActive(state);
|
|
|
+ }else{//is switch
|
|
|
+ hSwitch.setManualMode(true);
|
|
|
+ hSwitch.setManualState(state);
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ public boolean getState(int timeStep) {
|
|
|
+ return (type == HOLONELEMENT)?hElement.isActive():hSwitch.getState(timeStep);
|
|
|
+ }
|
|
|
+ public int getType() {
|
|
|
+ return type;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ private class Handle<T>{
|
|
|
+ public T object;
|
|
|
+ Handle(T object){
|
|
|
+ this.object = object;
|
|
|
+ }
|
|
|
+ public String toString() {
|
|
|
+ return object.toString();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+}
|