001 /**
002 * Copyright (C) 2007-2009, Jens Lehmann
003 *
004 * This file is part of DL-Learner.
005 *
006 * DL-Learner is free software; you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation; either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * DL-Learner is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014 * GNU General Public License for more details.
015 *
016 * You should have received a copy of the GNU General Public License
017 * along with this program. If not, see <http://www.gnu.org/licenses/>.
018 *
019 */
020 package org.dllearner.tools.protege;
021
022 import java.awt.AlphaComposite;
023 import java.awt.Color;
024 import java.awt.Dimension;
025 import java.awt.Graphics;
026 import java.awt.Graphics2D;
027 import java.awt.geom.Ellipse2D;
028 import java.util.Random;
029 import java.util.Set;
030 import java.util.Vector;
031
032 import javax.swing.JPanel;
033
034 import org.dllearner.core.EvaluatedDescription;
035 import org.dllearner.core.owl.Individual;
036 import org.dllearner.learningproblems.EvaluatedDescriptionClass;
037
038 /**
039 * This class draws the graphical coverage of a learned concept.
040 *
041 * @author Christian Koetteritzsch
042 *
043 */
044 public class GraphicalCoveragePanel extends JPanel {
045
046 private static final long serialVersionUID = 855436961912515267L;
047 private static final int HEIGHT = 150;
048 private static final int WIDTH = 150;
049 private static final int ELLIPSE_X_AXIS = 5;
050 private static final int ELLIPSE_Y_AXIS = 5;
051 private static final int MAX_NUMBER_OF_INDIVIDUAL_POINTS = 20;
052 private static final int PLUS_SIZE = 5;
053 private static final int SUBSTRING_SIZE = 25;
054 private static final int SPACE_SIZE = 7;
055 private static final String EQUI_STRING = "equivalent class";
056 private final String id;
057 private int shiftXAxis;
058 private int distortionOld;
059 private final Ellipse2D oldConcept;
060 private final Ellipse2D newConcept;
061
062 private EvaluatedDescription eval;
063 private final DLLearnerModel model;
064 private String conceptNew;
065 private final Vector<IndividualPoint> posCovIndVector;
066 private final Vector<IndividualPoint> posNotCovIndVector;
067 private final Vector<IndividualPoint> additionalIndividuals;
068 private final Vector<IndividualPoint> points;
069 private final Vector<String> conceptVector;
070 private final GraphicalCoveragePanelHandler handler;
071 private int adjustment;
072 private int shiftOldConcept;
073 private int shiftNewConcept;
074 private int shiftNewConceptX;
075 private int shiftCovered;
076 private int coveredIndividualSize;
077 private int additionalIndividualSize;
078 private int x1;
079 private int x2;
080 private int y1;
081 private int y2;
082 private int centerX;
083 private int centerY;
084 private final Random random;
085 private final Color darkGreen;
086 private final Color darkRed;
087 private final MoreDetailForSuggestedConceptsPanel panel;
088
089 /**
090 *
091 * This is the constructor for the GraphicalCoveragePanel.
092 *
093 * @param desc
094 * EvaluatedDescription
095 * @param m
096 * DLLearnerModel
097 * @param concept
098 * String
099 * @param p
100 * MoreDetailForSuggestedConceptsPanel
101 */
102 public GraphicalCoveragePanel(EvaluatedDescription desc, DLLearnerModel m,
103 String concept, MoreDetailForSuggestedConceptsPanel p) {
104 this.setPreferredSize(new Dimension(WIDTH, HEIGHT + 100));
105 this.setVisible(false);
106 this.setForeground(Color.GREEN);
107 this.repaint();
108 eval = desc;
109 model = m;
110 panel = p;
111 id = model.getID();
112 darkGreen = new Color(0, 100, 0);
113 darkRed = new Color(205, 0, 0);
114 random = new Random();
115 conceptNew = concept;
116 conceptVector = new Vector<String>();
117 posCovIndVector = new Vector<IndividualPoint>();
118 posNotCovIndVector = new Vector<IndividualPoint>();
119 additionalIndividuals = new Vector<IndividualPoint>();
120 points = new Vector<IndividualPoint>();
121 this.computeGraphics();
122 handler = new GraphicalCoveragePanelHandler(this, desc, model);
123 oldConcept = new Ellipse2D.Double(ELLIPSE_X_AXIS + (2 * adjustment),
124 ELLIPSE_Y_AXIS, WIDTH, HEIGHT);
125 newConcept = new Ellipse2D.Double(ELLIPSE_X_AXIS + shiftXAxis
126 + adjustment, ELLIPSE_Y_AXIS, WIDTH + distortionOld, HEIGHT
127 + distortionOld);
128 this.computeIndividualPoints();
129 this.addMouseMotionListener(handler);
130 this.addMouseListener(handler);
131 }
132
133 @Override
134 protected void paintComponent(Graphics g) {
135 if (eval != null) {
136 Graphics2D g2D;
137 g2D = (Graphics2D) g;
138
139 AlphaComposite ac = AlphaComposite.getInstance(
140 AlphaComposite.SRC_OVER, 0.5f);
141 g2D.setColor(Color.BLACK);
142 g2D.drawString(model.getOldConceptOWLAPI().toString(), 320, 10);
143 g2D.setColor(Color.ORANGE);
144 g2D.fillOval(310, 20, 9, 9);
145 g2D.setColor(Color.black);
146 int p = 30;
147 for (int i = 0; i < conceptVector.size(); i++) {
148 g2D.drawString(conceptVector.get(i), 320, p);
149 p = p + 20;
150 }
151 g2D.setColor(darkGreen);
152 g2D.drawString("*", 310, p+3);
153 g2D.setColor(Color.BLACK);
154 g2D.drawString("individuals covered by the new", 320, p);
155 p = p + 20;
156 g2D.drawString("class expression", 320, p);
157 p = p + 20;
158 g2D.setColor(darkRed);
159 g2D.drawString("*", 310, p+3);
160 g2D.setColor(Color.BLACK);
161 g2D.drawString("additional or not covered individuals", 320, p);
162 g2D.setColor(Color.YELLOW);
163 g2D.fill(oldConcept);
164 g2D.fillOval(310, 0, 9, 9);
165 g2D.setColor(Color.ORANGE);
166 g2D.setComposite(ac);
167 g2D.fill(newConcept);
168 g2D.setColor(Color.BLACK);
169
170 // Plus 1
171 if (coveredIndividualSize != model.getReasoner().getIndividuals(
172 model.getCurrentConcept()).size()
173 && coveredIndividualSize != 0) {
174 g2D.drawLine(x1 - 1 - shiftOldConcept, y1 - 1, x2 + 1
175 - shiftOldConcept, y1 - 1);
176 g2D.drawLine(x1 - shiftOldConcept, centerY - 1, x2
177 - shiftOldConcept, centerY - 1);
178 g2D.drawLine(x1 - shiftOldConcept, centerY, x2
179 - shiftOldConcept, centerY);
180 g2D.drawLine(x1 - shiftOldConcept, centerY + 1, x2
181 - shiftOldConcept, centerY + 1);
182 g2D.drawLine(x1 - 1 - shiftOldConcept, y2 + 1, x2 + 1
183 - shiftOldConcept, y2 + 1);
184
185 g2D.drawLine(x1 - 1 - shiftOldConcept, y1 - 1, x1 - 1
186 - shiftOldConcept, y2 + 1);
187 g2D.drawLine(centerX - 1 - shiftOldConcept, y1, centerX - 1
188 - shiftOldConcept, y2);
189 g2D.drawLine(centerX - shiftOldConcept, y1, centerX
190 - shiftOldConcept, y2);
191 g2D.drawLine(centerX + 1 - shiftOldConcept, y1, centerX + 1
192 - shiftOldConcept, y2);
193 g2D.drawLine(x2 + 1 - shiftOldConcept, y1 - 1, x2 + 1
194 - shiftOldConcept, y2 + 1);
195 }
196 // Plus 2
197
198 g2D.drawLine(x1 - 1 + shiftCovered, y1 - 1, x2 + 1 + shiftCovered,
199 y1 - 1);
200 g2D.drawLine(x1 + shiftCovered, centerY - 1, x2 + shiftCovered,
201 centerY - 1);
202 g2D
203 .drawLine(x1 + shiftCovered, centerY, x2 + shiftCovered,
204 centerY);
205 g2D.drawLine(x1 + shiftCovered, centerY + 1, x2 + shiftCovered,
206 centerY + 1);
207 g2D.drawLine(x1 - 1 + shiftCovered, y2 + 1, x2 + 1 + shiftCovered,
208 y2 + 1);
209
210 g2D.drawLine(x1 - 1 + shiftCovered, y1 - 1, x1 - 1 + shiftCovered,
211 y2 + 1);
212 g2D.drawLine(centerX - 1 + shiftCovered, y1, centerX - 1
213 + shiftCovered, y2);
214 g2D
215 .drawLine(centerX + shiftCovered, y1, centerX
216 + shiftCovered, y2);
217 g2D.drawLine(centerX + 1 + shiftCovered, y1, centerX + 1
218 + shiftCovered, y2);
219 g2D.drawLine(x2 + 1 + shiftCovered, y1 - 1, x2 + 1 + shiftCovered,
220 y2 + 1);
221
222 // Plus 3
223 if (coveredIndividualSize != model.getReasoner().getIndividuals(
224 model.getCurrentConcept()).size()) {
225 g2D.drawLine(x1 - 1 + shiftNewConcept, y1 - 1, x2 + 1
226 + shiftNewConcept, y1 - 1);
227 g2D.drawLine(x1 + shiftNewConcept, centerY - 1, x2
228 + shiftNewConcept, centerY - 1);
229 g2D.drawLine(x1 + shiftNewConcept, centerY, x2
230 + shiftNewConcept, centerY);
231 g2D.drawLine(x1 + shiftNewConcept, centerY + 1, x2
232 + shiftNewConcept, centerY + 1);
233 g2D.drawLine(x1 - 1 + shiftNewConcept, y2 + 1, x2 + 1
234 + shiftNewConcept, y2 + 1);
235
236 g2D.drawLine(x1 - 1 + shiftNewConcept, y1 - 1, x1 - 1
237 + shiftNewConcept, y2 + 1);
238 g2D.drawLine(centerX - 1 + shiftNewConcept, y1, centerX - 1
239 + shiftNewConcept, y2);
240 g2D.drawLine(centerX + shiftNewConcept, y1, centerX
241 + shiftNewConcept, y2);
242 g2D.drawLine(centerX + 1 + shiftNewConcept, y1, centerX + 1
243 + shiftNewConcept, y2);
244 g2D.drawLine(x2 + 1 + shiftNewConcept, y1 - 1, x2 + 1
245 + shiftNewConcept, y2 + 1);
246 }
247
248 if (((EvaluatedDescriptionClass) eval).getAddition() != 1.0
249 && ((EvaluatedDescriptionClass) eval).getCoverage() == 1.0) {
250 g2D.drawLine(x1 - 1 + shiftNewConceptX, y1 - 1
251 + shiftNewConcept, x2 + 1 + shiftNewConceptX, y1 - 1
252 + shiftNewConcept);
253 g2D.drawLine(x1 + shiftNewConceptX, centerY - 1
254 + shiftNewConcept, x2 + shiftNewConceptX, centerY - 1
255 + shiftNewConcept);
256 g2D.drawLine(x1 + shiftNewConceptX, centerY + shiftNewConcept,
257 x2 + shiftNewConceptX, centerY + shiftNewConcept);
258 g2D.drawLine(x1 + shiftNewConceptX, centerY + 1
259 + shiftNewConcept, x2 + shiftNewConceptX, centerY + 1
260 + shiftNewConcept);
261 g2D.drawLine(x1 - 1 + shiftNewConceptX, y2 + 1
262 + shiftNewConcept, x2 + 1 + shiftNewConceptX, y2 + 1
263 + shiftNewConcept);
264
265 g2D.drawLine(x1 - 1 + shiftNewConceptX, y1 - 1
266 + shiftNewConcept, x1 - 1 + shiftNewConceptX, y2 + 1
267 + shiftNewConcept);
268 g2D.drawLine(centerX - 1 + shiftNewConceptX, y1
269 + shiftNewConcept, centerX - 1 + shiftNewConceptX, y2
270 + shiftNewConcept);
271 g2D.drawLine(centerX + shiftNewConceptX, y1 + shiftNewConcept,
272 centerX + shiftNewConceptX, y2 + shiftNewConcept);
273 g2D.drawLine(centerX + 1 + shiftNewConceptX, y1
274 + shiftNewConcept, centerX + 1 + shiftNewConceptX, y2
275 + shiftNewConcept);
276 g2D.drawLine(x2 + 1 + shiftNewConceptX, y1 - 1
277 + shiftNewConcept, x2 + 1 + shiftNewConceptX, y2 + 1
278 + shiftNewConcept);
279 }
280
281 for (int i = 0; i < posCovIndVector.size(); i++) {
282 g2D.setColor(darkGreen);
283 g2D.draw(posCovIndVector.get(i).getIndividualPoint());
284 }
285
286 for (int i = 0; i < posNotCovIndVector.size(); i++) {
287 g2D.setColor(darkRed);
288 g2D.draw(posNotCovIndVector.get(i).getIndividualPoint());
289 }
290
291 for (int i = 0; i < additionalIndividuals.size(); i++) {
292 g2D.setColor(Color.BLACK);
293 g2D.draw(additionalIndividuals.get(i).getIndividualPoint());
294 }
295 this.setVisible(true);
296 panel.repaint();
297 }
298 }
299
300 private void computeGraphics() {
301 if (eval != null) {
302 this.setVisible(true);
303 panel.repaint();
304 additionalIndividualSize = ((EvaluatedDescriptionClass) eval)
305 .getAdditionalInstances().size();
306 distortionOld = 0;
307 adjustment = 0;
308 Ellipse2D old = new Ellipse2D.Double(ELLIPSE_X_AXIS, ELLIPSE_Y_AXIS,
309 WIDTH, HEIGHT);
310 x1 = (int) old.getCenterX() - PLUS_SIZE;
311 x2 = (int) old.getCenterX() + PLUS_SIZE;
312 y1 = (int) old.getCenterY() - PLUS_SIZE;
313 y2 = (int) old.getCenterY() + PLUS_SIZE;
314 centerX = (int) old.getCenterX();
315 centerY = (int) old.getCenterY();
316 double coverage = ((EvaluatedDescriptionClass) eval).getCoverage();
317 shiftXAxis = (int) Math.round(WIDTH * (1 - coverage));
318
319 if (additionalIndividualSize != 0 && ((EvaluatedDescriptionClass) eval).getCoverage() == 1.0 && ((EvaluatedDescriptionClass) eval).getAddition() < 1.0) {
320 distortionOld = (int) Math.round(WIDTH * 0.3);
321 Ellipse2D newer = new Ellipse2D.Double(ELLIPSE_X_AXIS + shiftXAxis,
322 ELLIPSE_Y_AXIS, WIDTH, HEIGHT);
323 adjustment = (int) Math.round(newer.getCenterY() / 4);
324 }
325 this.renderPlus();
326 }
327 }
328
329 private void renderPlus() {
330 if (eval != null) {
331 coveredIndividualSize = ((EvaluatedDescriptionClass) eval)
332 .getCoveredInstances().size();
333 double newConcepts = ((EvaluatedDescriptionClass) eval)
334 .getAddition();
335 double oldConcepts = ((EvaluatedDescriptionClass) eval)
336 .getCoverage();
337 shiftNewConcept = 0;
338 shiftOldConcept = 0;
339 shiftNewConceptX = 0;
340 shiftCovered = 0;
341 if (coveredIndividualSize == 0) {
342 shiftNewConcept = (int) Math.round((WIDTH / 2.0) * newConcepts);
343 } else if (additionalIndividualSize != coveredIndividualSize) {
344 shiftNewConcept = (int) Math.round((WIDTH / 2.0)
345 * (1.0 + (1.0 - oldConcepts)));
346 shiftOldConcept = (int) Math.round((WIDTH / 2.0) * oldConcepts);
347 shiftCovered = (int) Math.round((WIDTH / 2.0)
348 * (1 - oldConcepts));
349 }
350 if (((EvaluatedDescriptionClass) eval).getAddition() != 1.0 && ((EvaluatedDescriptionClass) eval)
351 .getCoverage() == 1.0) {
352 shiftCovered = (int) Math.round((WIDTH / 2.0) * 0.625);
353 shiftNewConceptX = shiftCovered;
354 shiftNewConcept = 2 * shiftNewConceptX;
355 }
356 }
357
358 int i = conceptNew.length();
359 while (i > 0) {
360 int sub = conceptNew.indexOf(" ");
361 String subString = conceptNew.substring(0, sub) + " ";
362 conceptNew = conceptNew.replace(conceptNew.substring(0, sub + 1),
363 "");
364 while (sub < SUBSTRING_SIZE) {
365 if (conceptNew.length() > 0 && conceptNew.contains(" ")) {
366 sub = conceptNew.indexOf(" ");
367 if (subString.length() + sub < SUBSTRING_SIZE) {
368 subString = subString + conceptNew.substring(0, sub)
369 + " ";
370 conceptNew = conceptNew.replace(conceptNew.substring(0,
371 sub + 1), "");
372 sub = subString.length();
373 } else {
374 break;
375 }
376 } else {
377 if (subString.length() + conceptNew.length() > SUBSTRING_SIZE
378 + SPACE_SIZE) {
379 conceptVector.add(subString);
380 subString = conceptNew;
381 conceptNew = "";
382 break;
383 } else {
384 subString = subString + conceptNew;
385 conceptNew = "";
386 break;
387 }
388 }
389 }
390 conceptVector.add(subString);
391 i = conceptNew.length();
392 }
393 }
394
395 private void computeIndividualPoints() {
396 if (eval != null) {
397 Set<Individual> posInd = ((EvaluatedDescriptionClass) eval)
398 .getCoveredInstances();
399 int i = 0;
400 double x = random.nextInt(300);
401 double y = random.nextInt(300);
402 boolean flag = true;
403 for (Individual ind : posInd) {
404 flag = true;
405 if (i < MAX_NUMBER_OF_INDIVIDUAL_POINTS) {
406 while (flag) {
407 if (newConcept.contains(x, y)
408 && oldConcept.contains(x, y)
409 && !(x >= this.getX1() + this.getShiftCovered()
410 && x <= this.getX2()
411 + this.getShiftCovered()
412 && y >= this.getY1() && y <= this
413 .getY2())) {
414 Set<String> uriString = model.getOntologyURIString();
415 for(String uri : uriString) {
416 if(ind.toString().contains(uri)) {
417 posCovIndVector.add(new IndividualPoint("*",
418 (int) x, (int) y, ind.toManchesterSyntaxString(uri, null)));
419 }
420 }
421 i++;
422 flag = false;
423
424 x = random.nextInt(300);
425 y = random.nextInt(300);
426 break;
427 } else {
428 x = random.nextInt(300);
429 y = random.nextInt(300);
430 }
431
432 }
433 }
434 }
435
436 Set<Individual> posNotCovInd = ((EvaluatedDescriptionClass) eval)
437 .getAdditionalInstances();
438 int j = 0;
439 x = random.nextInt(300);
440 y = random.nextInt(300);
441 for (Individual ind : posNotCovInd) {
442 flag = true;
443 if (j < MAX_NUMBER_OF_INDIVIDUAL_POINTS) {
444 while (flag) {
445 if (!oldConcept.contains(x, y)
446 && newConcept.contains(x, y)
447 && !(x >= this.getX1()
448 + this.getShiftNewConcept()
449 && x <= this.getX2()
450 + this.getShiftNewConcept()
451 && y >= this.getY1() && y <= this
452 .getY2())
453 && !(x >= this.getX1()
454 + this.getShiftNewConceptX()
455 && x <= this.getX2()
456 + this.getShiftNewConceptX()
457 && y >= this.getY1()
458 + this.getShiftNewConcept() && y <= this
459 .getY2()
460 + this.getShiftNewConcept())) {
461 if (id.equals(EQUI_STRING)) {
462 Set<String> uriString = model.getOntologyURIString();
463 for(String uri : uriString) {
464 if(ind.toString().contains(uri)) {
465 posNotCovIndVector.add(new IndividualPoint("*",
466 (int) x, (int) y, ind.toManchesterSyntaxString(uri, null)));
467 }
468 }
469 } else {
470 Set<String> uriString = model.getOntologyURIString();
471 for(String uri : uriString) {
472 if(ind.toString().contains(uri)) {
473 additionalIndividuals.add(new IndividualPoint("*",
474 (int) x, (int) y, ind.toManchesterSyntaxString(uri, null)));
475 }
476 }
477 }
478 j++;
479 flag = false;
480 x = random.nextInt(300);
481 y = random.nextInt(300);
482 break;
483 } else {
484 x = random.nextInt(300);
485 y = random.nextInt(300);
486 }
487
488 }
489 }
490 }
491
492 Set<Individual> notCovInd = model.getReasoner().getIndividuals(
493 model.getCurrentConcept());
494 notCovInd.removeAll(posInd);
495 int k = 0;
496 x = random.nextInt(300);
497 y = random.nextInt(300);
498 for (Individual ind : notCovInd) {
499 flag = true;
500 if (k < MAX_NUMBER_OF_INDIVIDUAL_POINTS) {
501 while (flag) {
502 if (oldConcept.contains(x, y)
503 && !newConcept.contains(x, y)
504 && !(x >= this.getX1()
505 - this.getShiftOldConcept()
506 && x <= this.getX2()
507 - this.getShiftOldConcept()
508 && y >= this.getY1() && y <= this
509 .getY2())) {
510 Set<String> uriString = model.getOntologyURIString();
511 for(String uri : uriString) {
512 if(ind.toString().contains(uri)) {
513 posNotCovIndVector.add(new IndividualPoint("*",
514 (int) x, (int) y, ind.toManchesterSyntaxString(uri, null)));
515 }
516 }
517 k++;
518 flag = false;
519 x = random.nextInt(300);
520 y = random.nextInt(300);
521 break;
522 } else {
523 x = random.nextInt(300);
524 y = random.nextInt(300);
525 }
526
527 }
528 }
529 }
530 points.addAll(posCovIndVector);
531 points.addAll(posNotCovIndVector);
532 points.addAll(additionalIndividuals);
533 }
534 }
535
536 /**
537 * This method returns a Vector of all individuals that are drawn in the
538 * panel.
539 *
540 * @return Vector of Individuals
541 */
542 public Vector<IndividualPoint> getIndividualVector() {
543 return points;
544 }
545
546 /**
547 * This method returns the GraphicalCoveragePanel.
548 *
549 * @return GraphicalCoveragePanel
550 */
551 public GraphicalCoveragePanel getGraphicalCoveragePanel() {
552 return this;
553 }
554
555 /**
556 * This method returns the MoreDetailForSuggestedConceptsPanel.
557 *
558 * @return MoreDetailForSuggestedConceptsPanel
559 */
560 public MoreDetailForSuggestedConceptsPanel getMoreDetailForSuggestedConceptsPanel() {
561 return panel;
562 }
563
564 /**
565 * Returns the min. x value of the plus.
566 *
567 * @return int min X Value
568 */
569 public int getX1() {
570 return x1;
571 }
572
573 /**
574 * Returns the max. x value of the plus.
575 *
576 * @return int max X Value
577 */
578 public int getX2() {
579 return x2;
580 }
581
582 /**
583 * Returns the min. y value of the plus.
584 *
585 * @return int min Y Value
586 */
587 public int getY1() {
588 return y1;
589 }
590
591 /**
592 * Returns the max. y value of the plus.
593 *
594 * @return int max Y Value
595 */
596 public int getY2() {
597 return y2;
598 }
599
600 /**
601 *
602 * @return
603 */
604 public int getShiftOldConcept() {
605 return shiftOldConcept;
606 }
607
608 /**
609 *
610 * @return
611 */
612 public int getShiftCovered() {
613 return shiftCovered;
614 }
615
616 /**
617 *
618 * @return
619 */
620 public int getShiftNewConcept() {
621 return shiftNewConcept;
622 }
623
624 /**
625 *
626 * @return
627 */
628 public int getShiftNewConceptX() {
629 return shiftNewConceptX;
630 }
631
632 /**
633 * Unsets the panel after plugin is closed.
634 */
635 public void unsetPanel() {
636 this.removeAll();
637 eval = null;
638 }
639
640 /**
641 * Returns the currently selected evaluated description.
642 *
643 * @return EvaluatedDescription
644 */
645 public EvaluatedDescription getEvaluateddescription() {
646 return eval;
647 }
648 }