1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 package org.diyefi.openlogviewer.optionpanel;
24
25 import java.awt.Color;
26 import java.awt.Component;
27 import java.awt.Dimension;
28 import java.awt.Point;
29 import java.awt.event.ActionEvent;
30 import java.awt.event.ActionListener;
31 import java.awt.event.ContainerEvent;
32 import java.awt.event.ContainerListener;
33 import java.awt.event.MouseEvent;
34 import java.awt.event.MouseListener;
35 import java.awt.event.MouseMotionAdapter;
36
37 import java.util.ArrayList;
38 import java.util.Iterator;
39
40 import javax.swing.JFrame;
41 import javax.swing.JPanel;
42 import javax.swing.JButton;
43 import javax.swing.JLayeredPane;
44 import javax.swing.JScrollPane;
45 import javax.swing.JLabel;
46 import javax.swing.JInternalFrame;
47 import javax.swing.JTextField;
48 import javax.swing.JColorChooser;
49 import javax.swing.BorderFactory;
50
51 import org.diyefi.openlogviewer.OpenLogViewer;
52 import org.diyefi.openlogviewer.genericlog.GenericDataElement;
53 import org.diyefi.openlogviewer.genericlog.GenericLog;
54 import org.diyefi.openlogviewer.propertypanel.SingleProperty;
55
56 public class OptionFrameV2 extends JFrame {
57 private static final long serialVersionUID = 1L;
58
59 private static final int NUMBER_OF_COLS_OF_FREEEMS_FIELDS = 8;
60 private static final int HEIGHT_IN_FIELDS = 12;
61 private static final int NUMBER_OF_ADD_BUTTONS = 1;
62
63 private static final int WIDTH_OF_BOXES = NUMBER_OF_COLS_OF_FREEEMS_FIELDS;
64 private static final int HEIGHT_OF_BOXES = 2;
65 private static final int MAX_NUMBER_OF_BOXES = WIDTH_OF_BOXES * HEIGHT_OF_BOXES;
66
67
68 private static final int WTF = 4;
69 private static final int WTF2 = 3;
70
71 private static final int COMP_HEIGHT = 20;
72 private static final int COMP_WIDTH = 200;
73 private static final int PANEL_WIDTH = 140;
74 private static final int PANEL_HEIGHT = 120;
75 private static final int SCROLL_BAR_SIZE = 16;
76
77
78 private static final int WIDTH_OF_WINDOW = (NUMBER_OF_COLS_OF_FREEEMS_FIELDS * PANEL_WIDTH);
79 private static final int HEIGHT_OF_WINDOW = ((PANEL_HEIGHT * HEIGHT_OF_BOXES) + (COMP_HEIGHT * (HEIGHT_IN_FIELDS + NUMBER_OF_ADD_BUTTONS)));
80
81 private JFrame thisRef;
82 private JPanel inactiveHeaders;
83 private ModifyGraphPane infoPanel;
84 private JButton addDivisionButton;
85
86 private JLayeredPane layeredPane;
87 private ArrayList<JPanel> activePanelList;
88
89 public OptionFrameV2() {
90 super("Graphing Option Pane");
91 this.setSize(WIDTH_OF_WINDOW + WTF2, HEIGHT_OF_WINDOW + COMP_HEIGHT + SCROLL_BAR_SIZE + WTF);
92 this.setPreferredSize(this.getSize());
93
94 this.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
95 thisRef = this;
96 activePanelList = new ArrayList<JPanel>();
97 layeredPane = new JLayeredPane();
98 layeredPane.setPreferredSize(new Dimension(WIDTH_OF_WINDOW, HEIGHT_OF_WINDOW));
99
100 JScrollPane scroll = new JScrollPane(layeredPane);
101
102 inactiveHeaders = initHeaderPanel();
103 layeredPane.add(inactiveHeaders);
104 infoPanel = new ModifyGraphPane();
105 this.add(infoPanel);
106
107 this.add(scroll);
108 addActiveHeaderPanel();
109 }
110
111 private JPanel initHeaderPanel() {
112 JPanel ih = new JPanel();
113 ih.setLayout(null);
114 ih.setName("Drop InactiveHeaderPanel");
115 this.addDivisionButton = new JButton("Add Division");
116 addDivisionButton.setBounds(0, 0, PANEL_WIDTH, COMP_HEIGHT);
117 addDivisionButton.addActionListener(addDivisionListener);
118 ih.add(addDivisionButton);
119 ih.setBounds(0, 0, 1280, (COMP_HEIGHT * (HEIGHT_IN_FIELDS + NUMBER_OF_ADD_BUTTONS)));
120 return ih;
121 }
122
123 private ActionListener addDivisionListener = new ActionListener() {
124 @Override
125 public void actionPerformed(ActionEvent e) {
126 addActiveHeaderPanel();
127 }
128 };
129
130 private ActionListener remDivisionListener = new ActionListener() {
131 @Override
132 public void actionPerformed(final ActionEvent e) {
133 remActiveHeaderPanel(e);
134 }
135 };
136
137 private ContainerListener addRemoveListener = new ContainerListener() {
138 @Override
139 public void componentAdded(final ContainerEvent e) {
140
141 if (e.getChild() != null) {
142 if (e.getChild() instanceof ActiveHeaderLabel) {
143 ((ActiveHeaderLabel) e.getChild()).setEnabled(true);
144 ((ActiveHeaderLabel) e.getChild()).setSelected(true);
145 ((ActiveHeaderLabel) e.getChild()).getGDE().setSplitNumber(activePanelList.indexOf(e.getChild().getParent()) + 1);
146 }
147 }
148 }
149
150 @Override
151 public void componentRemoved(final ContainerEvent e) {
152 if (e.getChild() != null) {
153 if (e.getChild() instanceof ActiveHeaderLabel) {
154 ((ActiveHeaderLabel) e.getChild()).setEnabled(false);
155 ((ActiveHeaderLabel) e.getChild()).setSelected(false);
156 }
157 for (int i = 0; i < e.getContainer().getComponentCount(); i++) {
158 if (e.getContainer().getComponent(i) instanceof ActiveHeaderLabel) {
159 e.getContainer().getComponent(i).setLocation(0, i * COMP_HEIGHT);
160 }
161 }
162 }
163 }
164 };
165
166 private void addActiveHeaderPanel() {
167 if (activePanelList.size() < MAX_NUMBER_OF_BOXES) {
168 final int row = activePanelList.size() / WIDTH_OF_BOXES;
169 final int col = activePanelList.size() % WIDTH_OF_BOXES;
170 final JPanel activePanel = new JPanel();
171 activePanelList.add(activePanel);
172 if (OpenLogViewer.getInstance() != null) {
173 OpenLogViewer.getInstance().getMultiGraphLayeredPane().setTotalSplits(activePanelList.size());
174 }
175 activePanel.setLayout(null);
176 activePanel.setName("Drop ActivePanel " + (activePanelList.indexOf(activePanel) + 1));
177 activePanel.addContainerListener(addRemoveListener);
178
179 activePanel.setBounds((col * PANEL_WIDTH), inactiveHeaders.getHeight() + PANEL_HEIGHT * row, PANEL_WIDTH, PANEL_HEIGHT);
180 activePanel.setBackground(Color.DARK_GRAY);
181 final JButton removeButton = new JButton("Remove");
182 removeButton.setToolTipText("Click Here to remove this division and associated Graphs");
183 removeButton.setBounds(0, 0, PANEL_WIDTH, COMP_HEIGHT);
184 removeButton.addActionListener(remDivisionListener);
185 activePanel.add(removeButton);
186 layeredPane.add(activePanel);
187
188 if (activePanelList.size() == MAX_NUMBER_OF_BOXES) {
189 addDivisionButton.setEnabled(false);
190 }
191 }
192 }
193
194 private void remActiveHeaderPanel(final ActionEvent e) {
195 final JPanel panel = (JPanel) ((JButton) e.getSource()).getParent();
196 activePanelList.remove(panel);
197 OpenLogViewer.getInstance().getMultiGraphLayeredPane().setTotalSplits(activePanelList.size());
198
199 for (int i = 0; i < panel.getComponentCount();) {
200 if (panel.getComponent(i) instanceof ActiveHeaderLabel) {
201 final ActiveHeaderLabel GCB = (ActiveHeaderLabel) panel.getComponent(i);
202 GCB.getInactivePanel().add(GCB);
203 GCB.setLocation(GCB.getInactiveLocation());
204 GCB.setSelected(false);
205 } else if (panel.getComponent(i) instanceof JButton) {
206 panel.remove(panel.getComponent(i));
207 } else {
208 i++;
209 }
210 }
211
212 panel.getParent().remove(panel);
213 for (int i = 0; i < activePanelList.size(); i++) {
214 final int row = i / WIDTH_OF_BOXES;
215 final int col = i % WIDTH_OF_BOXES;
216 activePanelList.get(i).setLocation((col * PANEL_WIDTH), inactiveHeaders.getHeight() + PANEL_HEIGHT * row);
217 }
218
219 if (!addDivisionButton.isEnabled()) {
220 addDivisionButton.setEnabled(true);
221 }
222
223
224 if (activePanelList.size() > 1) {
225 for (int i = 0; i < activePanelList.size(); i++) {
226 final JPanel active = activePanelList.get(i);
227 if (active.getComponentCount() > 1) {
228 for (int j = 0; j < active.getComponentCount(); j++) {
229 if (active.getComponent(j) instanceof ActiveHeaderLabel) {
230 ((ActiveHeaderLabel) active.getComponent(j)).getGDE().setSplitNumber(i + 1);
231 }
232 }
233 }
234 }
235 }
236
237 if (activePanelList.isEmpty()) {
238 addActiveHeaderPanel();
239 }
240
241 this.repaint();
242 }
243
244 private MouseMotionAdapter labelAdapter = new MouseMotionAdapter() {
245
246 @Override
247 public void mouseDragged(final MouseEvent e) {
248 final Component c = e.getComponent();
249 final ActiveHeaderLabel GCB = (ActiveHeaderLabel) c;
250 GCB.setDragging(true);
251 final Point pointNow = layeredPane.getMousePosition();
252 final Component parent = c.getParent();
253 if ((e.getModifiers() == 16) && (parent != null) && (pointNow != null)) {
254 if (!parent.contains(pointNow.x - parent.getX(), pointNow.y - parent.getY())) {
255 final Component parentsParent = parent.getParent();
256 final Component cn = parentsParent.getComponentAt(pointNow);
257 if (cn instanceof JPanel) {
258 final JPanel j = (JPanel) cn;
259 if (j.getName().contains("Drop")) {
260 j.add(c);
261
262 c.setLocation(
263
264 pointNow.x - parent.getX() - (c.getWidth() / 2),
265 pointNow.y - parent.getY() - (c.getHeight() / 2));
266 }
267 }
268 } else {
269 c.setLocation(c.getX() + e.getX() - (c.getWidth() / 2), c.getY() + e.getY() - (c.getHeight() / 2));
270 }
271 thisRef.repaint();
272 }
273 }
274 };
275
276 private boolean place(final ActiveHeaderLabel GCB) {
277 final int x = 0;
278 int y = COMP_HEIGHT;
279 while (y < GCB.getParent().getHeight()) {
280 if (GCB.getParent().getComponentAt(x, y) == GCB.getParent() || GCB.getParent().getComponentAt(x, y) == GCB) {
281 GCB.setLocation(x, y);
282 return true;
283 }
284 y = y + COMP_HEIGHT;
285 }
286 return false;
287 }
288
289 public void updateFromLog(final GenericLog datalog) {
290
291 while (activePanelList.size() > 0) {
292 activePanelList.get(0).removeAll();
293 layeredPane.remove(activePanelList.get(0));
294 activePanelList.remove(activePanelList.get(0));
295 }
296
297 addDivisionButton.setEnabled(true);
298
299 if (inactiveHeaders.getComponentCount() > 1) {
300 inactiveHeaders.removeAll();
301 inactiveHeaders.add(this.addDivisionButton);
302 }
303 this.addActiveHeaderPanel();
304
305 final ArrayList<ActiveHeaderLabel> tmpList = new ArrayList<ActiveHeaderLabel>();
306 final Iterator<String> headers = datalog.keySet().iterator();
307 String header = "";
308 ActiveHeaderLabel toBeAdded = null;
309
310 while (headers.hasNext()) {
311 header = headers.next();
312 final GenericDataElement GDE = datalog.get(header);
313 toBeAdded = new ActiveHeaderLabel();
314
315 toBeAdded.setName(header);
316 toBeAdded.setText(header);
317 toBeAdded.setRef(GDE);
318 toBeAdded.setEnabled(false);
319 toBeAdded.addMouseMotionListener(labelAdapter);
320 if (checkForProperties(toBeAdded, GDE)) {
321 toBeAdded.setBackground(GDE.getDisplayColor());
322 }
323 tmpList.add(toBeAdded);
324 }
325
326 int j = 0;
327 int leftSide = 0;
328 for (int it = 0; it < tmpList.size(); it++) {
329 if (COMP_HEIGHT + (COMP_HEIGHT * (j + 1)) > inactiveHeaders.getHeight()) {
330 j = 0;
331 leftSide += PANEL_WIDTH;
332 }
333
334 tmpList.get(it).setBounds(leftSide, (COMP_HEIGHT + (COMP_HEIGHT * j)), PANEL_WIDTH, COMP_HEIGHT);
335 inactiveHeaders.add(tmpList.get(it));
336 j++;
337 }
338
339 this.repaint();
340 this.setDefaultCloseOperation(JFrame.ICONIFIED);
341 this.setVisible(true);
342 }
343
344 private boolean checkForProperties(ActiveHeaderLabel GCB, GenericDataElement GDE) {
345 for (int i = 0; i < OpenLogViewer.getInstance().getProperties().size(); i++) {
346 if (OpenLogViewer.getInstance().getProperties().get(i).getHeader().equals(GDE.getName())) {
347 GDE.setDisplayColor(OpenLogViewer.getInstance().getProperties().get(i).getColor());
348 GDE.setDisplayMaxValue(OpenLogViewer.getInstance().getProperties().get(i).getMax());
349 GDE.setDisplayMinValue(OpenLogViewer.getInstance().getProperties().get(i).getMin());
350 GDE.setSplitNumber(OpenLogViewer.getInstance().getProperties().get(i).getSplit());
351
352 if (OpenLogViewer.getInstance().getProperties().get(i).isActive()) {
353 return true;
354 }
355 }
356 }
357 return false;
358 }
359
360 private class ModifyGraphPane extends JInternalFrame {
361 private static final long serialVersionUID = 1L;
362
363 GenericDataElement GDE;
364 ActiveHeaderLabel AHL;
365 private JLabel minLabel;
366 private JLabel maxLabel;
367 private JTextField minField;
368 private JTextField maxField;
369 private JButton resetButton;
370 private JButton applyButton;
371 private JButton saveButton;
372 private JButton colorButton;
373
374 private ActionListener resetButtonListener = new ActionListener() {
375 @Override
376 public void actionPerformed(final ActionEvent e) {
377 if (GDE != null) {
378 GDE.reset();
379 minField.setText(Double.toString(GDE.getDisplayMinValue()));
380 maxField.setText(Double.toString(GDE.getDisplayMaxValue()));
381 }
382 }
383 };
384
385 private ActionListener applyButtonListener = new ActionListener() {
386 @Override
387 public void actionPerformed(final ActionEvent e) {
388 if (GDE != null) {
389 changeGDEValues();
390 }
391 }
392 };
393
394 private ActionListener saveButtonListener = new ActionListener() {
395
396 @Override
397 public void actionPerformed(final ActionEvent e) {
398 if (GDE != null) {
399 changeGDEValues();
400 OpenLogViewer.getInstance().getPropertyPane().addPropertyAndSave(new SingleProperty(GDE));
401 }
402 }
403 };
404
405 private ActionListener colorButtonListener = new ActionListener() {
406
407 @Override
408 public void actionPerformed(final ActionEvent e) {
409 Color c = JColorChooser.showDialog(
410 new JFrame(),
411 "Choose Background Color",
412 colorButton.getForeground());
413 if (c != null) {
414 colorButton.setForeground(c);
415 }
416 }
417 };
418
419 public ModifyGraphPane() {
420 this.setName("InfoPanel");
421 minLabel = new JLabel("Display Min:");
422 maxLabel = new JLabel("Display Max:");
423 minField = new JTextField(10);
424 maxField = new JTextField(10);
425 resetButton = new JButton("Reset Min/Max");
426 resetButton.addActionListener(resetButtonListener);
427
428 applyButton = new JButton("Apply");
429 applyButton.addActionListener(applyButtonListener);
430
431 saveButton = new JButton("Save");
432 saveButton.addActionListener(saveButtonListener);
433
434 colorButton = new JButton("Color");
435 colorButton.addActionListener(colorButtonListener);
436
437 minLabel.setBounds(0, 0, COMP_WIDTH / 2, COMP_HEIGHT);
438 minField.setBounds(100, 0, COMP_WIDTH / 2, COMP_HEIGHT);
439 maxLabel.setBounds(0, 20, COMP_WIDTH / 2, COMP_HEIGHT);
440 maxField.setBounds(100, 20, COMP_WIDTH / 2, COMP_HEIGHT);
441 colorButton.setBounds(0, 40, COMP_WIDTH, COMP_HEIGHT);
442 applyButton.setBounds(0, 60, COMP_WIDTH / 2, COMP_HEIGHT);
443 saveButton.setBounds(100, 60, COMP_WIDTH / 2, COMP_HEIGHT);
444 resetButton.setBounds(0, 80, COMP_WIDTH, COMP_HEIGHT);
445
446 this.setLayout(null);
447
448 this.add(minLabel);
449 this.add(minField);
450 this.add(maxLabel);
451 this.add(maxField);
452 this.add(colorButton);
453 this.add(applyButton);
454 this.add(saveButton);
455 this.add(resetButton);
456 this.setBounds(500, 180, 210, 133);
457 this.setMaximizable(false);
458 this.setDefaultCloseOperation(JInternalFrame.HIDE_ON_CLOSE);
459 this.setClosable(true);
460 }
461
462 public void setGDE(GenericDataElement gde, ActiveHeaderLabel ahl) {
463 this.GDE = gde;
464 this.AHL = ahl;
465 this.setTitle(GDE.getName());
466 minField.setText(String.valueOf(GDE.getDisplayMinValue()));
467 maxField.setText(String.valueOf(GDE.getDisplayMaxValue()));
468 colorButton.setForeground(GDE.getDisplayColor());
469 }
470
471 private void changeGDEValues() {
472 try {
473 GDE.setDisplayMaxValue(Double.parseDouble(maxField.getText()));
474 } catch (Exception ex) {
475 throw new RuntimeException("TODO: do something with Auto field");
476 }
477
478 try {
479 GDE.setDisplayMinValue(Double.parseDouble(minField.getText()));
480 } catch (Exception ex) {
481 throw new RuntimeException("TODO: do something with Auto field");
482 }
483
484 if (!GDE.getDisplayColor().equals(colorButton.getForeground())) {
485 GDE.setDisplayColor(colorButton.getForeground());
486 AHL.setForeground(colorButton.getForeground());
487 }
488 }
489 }
490
491 private class ActiveHeaderLabel extends JLabel implements Comparable<Object> {
492 private static final long serialVersionUID = 1L;
493
494 private GenericDataElement GDE;
495 private Point inactiveLocation;
496 private JPanel previousPanel;
497 private JPanel inactivePanel;
498 private boolean dragging;
499 private boolean selected;
500 private MouseListener selectedListener = new MouseListener() {
501
502 @Override
503 public void mouseClicked(MouseEvent e) {
504 if (e.getModifiers() == 16) {
505 setSelected(!selected);
506 } else if (e.getModifiers() == 18) {
507 infoPanel.setGDE(GDE, (ActiveHeaderLabel) e.getSource());
508 if (!infoPanel.isVisible()) {
509 infoPanel.setVisible(true);
510 }
511 }
512 }
513
514 @Override
515 public void mouseEntered(MouseEvent e) {
516 }
517
518 @Override
519 public void mouseExited(MouseEvent e) {
520 }
521
522 @Override
523 public void mousePressed(MouseEvent e) {
524 ActiveHeaderLabel GCB = (ActiveHeaderLabel) e.getSource();
525 GCB.setPreviousPanel((JPanel) GCB.getParent());
526 }
527
528 @Override
529 public void mouseReleased(MouseEvent e) {
530 ActiveHeaderLabel GCB = (ActiveHeaderLabel) e.getSource();
531 if (GCB.isDragging()) {
532 if (GCB.getParent() == inactiveHeaders) {
533 GCB.setLocation(GCB.getInactiveLocation());
534 GCB.setSelected(false);
535 GCB.setEnabled(false);
536 } else {
537 if (!place(GCB)) {
538 if (GCB.getPreviousPanel() != GCB.getParent()) {
539 GCB.getPreviousPanel().add(GCB);
540 place(GCB);
541 }
542 if (GCB.getPreviousPanel() == GCB.getInactivePanel()) {
543 GCB.setLocation(GCB.getInactiveLocation());
544 GCB.setEnabled(false);
545 GCB.setSelected(false);
546 } else {
547 place(GCB);
548 }
549 thisRef.repaint();
550 }
551 }
552 GCB.setDragging(false);
553 }
554 }
555 };
556
557 public ActiveHeaderLabel() {
558 super();
559 addMouseListener(selectedListener);
560 super.setOpaque(false);
561 inactivePanel = inactiveHeaders;
562 dragging = false;
563 selected = false;
564 super.setBorder(BorderFactory.createEtchedBorder(Color.lightGray, Color.white));
565 }
566
567 @Override
568 public void setBounds(int x, int y, int widht, int height) {
569 super.setBounds(x, y, widht, height);
570 if (inactiveLocation == null) {
571 inactiveLocation = new Point(x, y);
572 }
573 }
574
575 public void setRef(GenericDataElement GDE) {
576 this.GDE = GDE;
577
578
579 this.setToolTipTextPreliminary();
580
581 }
582
583 @Override
584 public String getToolTipText(MouseEvent e) {
585 this.setToolTipTextFinal();
586 return getToolTipText();
587 }
588
589 public void setToolTipTextFinal() {
590 this.setToolTipText("<HTML> Data Stream: </b>" + GDE.getName()
591 + "</b><br>Min Value: <b>" + GDE.getMinValue()
592 + "</b><br>Min Display: <b>" + GDE.getDisplayMinValue()
593 + "</b><br>Max Value: <b>" + GDE.getMaxValue()
594 + "</b><br>Max Display: <b>" + GDE.getDisplayMaxValue()
595 + "<br>To modify Min and Max values for scaling purposes Ctrl+LeftClick</HTML>");
596 }
597
598 public void setToolTipTextPreliminary() {
599 this.setToolTipText("<HTML> Data Stream: </b>" + GDE.getName()
600 + "</b><br>Min Value: <b> -.-"
601 + "</b><br>Max Value: <b> -.-"
602 + "</b><br>Min Display: <b> -.-"
603 + "</b><br>Max Display: <b> -.-"
604 + "<br>To modify Min and Max values for scaling purposes Ctrl+LeftClick</HTML>");
605 }
606
607 public GenericDataElement getGDE() {
608 return GDE;
609 }
610
611 public JPanel getPreviousPanel() {
612 return previousPanel;
613 }
614
615 public void setPreviousPanel(JPanel previousPanel) {
616 this.previousPanel = previousPanel;
617 }
618
619 public Point getInactiveLocation() {
620 return inactiveLocation;
621 }
622
623 public JPanel getInactivePanel() {
624 return inactivePanel;
625 }
626
627 public boolean isDragging() {
628 return dragging;
629 }
630
631 public void setDragging(boolean dragging) {
632 this.dragging = dragging;
633 }
634
635 public void setSelected(boolean selected) {
636 if (this.isEnabled()) {
637 this.selected = selected;
638 } else {
639 this.selected = false;
640 }
641 addRemGraph();
642 }
643
644 private void addRemGraph() {
645 if (selected) {
646 this.setForeground(GDE.getDisplayColor());
647 this.repaint();
648 OpenLogViewer.getInstance().getMultiGraphLayeredPane().addGraph(this.getName());
649 } else {
650 this.setForeground(GDE.getDisplayColor().darker().darker());
651 if (OpenLogViewer.getInstance().getMultiGraphLayeredPane().removeGraph(this.getName())) {
652 OpenLogViewer.getInstance().getMultiGraphLayeredPane().repaint();
653 }
654 }
655 }
656
657 @Override
658 public int compareTo(Object o) {
659 if (o instanceof ActiveHeaderLabel) {
660 ActiveHeaderLabel GCB = (ActiveHeaderLabel) o;
661 return this.GDE.compareTo(GCB.getGDE());
662 } else {
663 return -1;
664 }
665 }
666 }
667 }