1 /* $RCSfile$
2  * $Author$
3  * $Date$
4  * $Revision$
5  *
6  * Copyright (C) 2005  The Jmol Development Team
7  *
8  * Contact: jmol-developers@lists.sf.net
9  *
10  *  This library is free software; you can redistribute it and/or
11  *  modify it under the terms of the GNU Lesser General Public
12  *  License as published by the Free Software Foundation; either
13  *  version 2.1 of the License, or (at your option) any later version.
14  *
15  *  This library is distributed in the hope that it will be useful,
16  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
17  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18  *  Lesser General Public License for more details.
19  *
20  *  You should have received a copy of the GNU Lesser General Public
21  *  License along with this library; if not, write to the Free Software
22  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
23  *  02110-1301, USA.
24  */
25 
26 package org.jmol.util;
27 
28 import java.util.Iterator;
29 import java.util.Map;
30 
31 import javajs.util.AU;
32 import javajs.util.Lst;
33 import javajs.util.M34;
34 import javajs.util.PT;
35 import javajs.util.Quat;
36 import javajs.util.SB;
37 import javajs.util.A4;
38 import javajs.util.M3;
39 import javajs.util.M4;
40 import javajs.util.P3;
41 import javajs.util.P4;
42 import javajs.util.T3;
43 import javajs.util.V3;
44 
45 import org.jmol.api.JmolDataManager;
46 import javajs.util.BS;
47 import org.jmol.script.SV;
48 import org.jmol.script.T;
49 
50 
51 
52 public class Escape {
53 
escapeColor(int argb)54   public static String escapeColor(int argb) {
55     return (argb == 0 ? null  :  "[x" + getHexColorFromRGB(argb) + "]");
56   }
57 
getHexColorFromRGB(int argb)58   public static String getHexColorFromRGB(int argb) {
59     if (argb == 0)
60       return null;
61     String r  = "00" + Integer.toHexString((argb >> 16) & 0xFF);
62     r = r.substring(r.length() - 2);
63     String g  = "00" + Integer.toHexString((argb >> 8) & 0xFF);
64     g = g.substring(g.length() - 2);
65     String b  = "00" + Integer.toHexString(argb & 0xFF);
66     b = b.substring(b.length() - 2);
67     return r + g + b;
68   }
69 
70 
71   /**
72    * must be its own, because of the possibility of being null
73    * @param xyz
74    * @return  {x y z}
75    */
eP(T3 xyz)76   public static String eP(T3 xyz) {
77     if (xyz == null)
78       return "null";
79     return "{" + xyz.x + " " + xyz.y + " " + xyz.z + "}";
80   }
81 
matrixToScript(Object m)82   public static String matrixToScript(Object m) {
83     return PT.replaceAllCharacters(m.toString(), "\n\r ","").replace('\t',' ');
84   }
85 
eP4(P4 x)86   public static String eP4(P4 x) {
87     return "{" + x.x + " " + x.y + " " + x.z + " " + x.w + "}";
88   }
89 
drawQuat(Quat q, String prefix, String id, P3 ptCenter, float scale)90   public static String drawQuat(Quat q, String prefix, String id, P3 ptCenter,
91                          float scale) {
92     String strV = " VECTOR " + eP(ptCenter) + " ";
93     if (scale == 0)
94       scale = 1f;
95     return "draw " + prefix + "x" + id + strV
96         + eP(q.getVectorScaled(0, scale)) + " color red\n"
97         + "draw " + prefix + "y" + id + strV
98         + eP(q.getVectorScaled(1, scale)) + " color green\n"
99         + "draw " + prefix + "z" + id + strV
100         + eP(q.getVectorScaled(2, scale)) + " color blue\n";
101   }
102 
103   @SuppressWarnings("unchecked")
e(Object x)104   public static String e(Object x) {
105     if (x == null)
106       return "null";
107     if (PT.isNonStringPrimitive(x))
108       return x.toString();
109     if (x instanceof String)
110       return PT.esc((String) x);
111     if (x instanceof Lst<?>)
112       return eV((Lst<SV>) x);
113     if (x instanceof Map)
114       return escapeMap((Map<String, Object>) x);
115     if (x instanceof BS)
116       return eBS((BS) x);
117     if (x instanceof P4)
118       return eP4((P4) x);
119     if (x instanceof T3)
120       return eP((T3) x);
121     if (AU.isAP(x))
122       return eAP((T3[]) x);
123     if (AU.isAS(x))
124       return eAS((String[]) x, true);
125     if (x instanceof M34)
126       return PT.rep(PT.rep(x.toString(), "[\n  ", "["), "] ]", "]]");
127     if (AU.isAFF(x)) {
128       // for isosurface functionXY
129       float[][] ff = (float[][])x;
130       SB sb = new SB().append("[");
131       String sep = "";
132       for (int i = 0; i < ff.length; i++) {
133         sb.append(sep).append(eAF(ff[i]));
134         sep = ",";
135       }
136       sb.append("]");
137       return sb.toString();
138     }
139     if (x instanceof A4) {
140       A4 a = (A4) x;
141       return "{" + a.x + " " + a.y + " " + a.z + " " + (float) (a.angle * 180d/Math.PI) + "}";
142     }
143     if (x instanceof Quat)
144       return ((Quat) x).toString();
145     String s = PT.nonArrayString(x);
146     return (s == null ? PT.toJSON(null, x) : s);
147   }
148 
eV(Lst<SV> list)149   public static String eV(Lst<SV> list) {
150     if (list == null)
151       return PT.esc("");
152     SB s = new SB();
153     s.append("[");
154     for (int i = 0; i < list.size(); i++) {
155       if (i > 0)
156         s.append(", ");
157       s.append(escapeNice(list.get(i).asString()));
158     }
159     s.append("]");
160     return s.toString();
161   }
162 
escapeMap(Map<String, Object> ht)163   public static String escapeMap(Map<String, Object> ht) {
164     SB sb = new SB();
165     sb.append("{ ");
166     String sep = "";
167     for (Map.Entry<String, Object> entry : ht.entrySet()) {
168       String key = entry.getKey();
169       sb.append(sep).append(PT.esc(key)).appendC(':');
170       Object val = entry.getValue();
171       if (!(val instanceof SV))
172         val = SV.getVariable(val);
173       sb.append(((SV)val).escape());
174       sep = ",";
175     }
176     sb.append(" }");
177     return sb.toString();
178   }
179 
180   /**
181    *
182    * @param f
183    * @param asArray -- FALSE allows bypassing of escape(Object f); TRUE: unnecssary
184    * @return tabular string
185    */
escapeFloatA(float[] f, boolean asArray)186   public static String escapeFloatA(float[] f, boolean asArray) {
187     if (asArray)
188       return PT.toJSON(null, f); // or just use escape(f)
189     SB sb = new SB();
190     for (int i = 0; i < f.length; i++) {
191       if (i > 0)
192         sb.appendC('\n');
193       sb.appendF(f[i]);
194     }
195     return sb.toString();
196   }
197 
escapeFloatAA(float[][] f, boolean addSemi)198   public static String escapeFloatAA(float[][] f, boolean addSemi) {
199     SB sb = new SB();
200     String eol = (addSemi ? ";\n" : "\n");
201     for (int i = 0; i < f.length; i++)
202       if (f[i] != null) {
203         if (i > 0)
204           sb.append(eol);
205         for (int j = 0; j < f[i].length; j++)
206           sb.appendF(f[i][j]).appendC('\t');
207       }
208     return sb.toString();
209   }
210 
escapeFloatAAA(float[][][] f, boolean addSemi)211   public static String escapeFloatAAA(float[][][] f, boolean addSemi) {
212     SB sb = new SB();
213     String eol = (addSemi ? ";\n" : "\n");
214     if (f[0] == null || f[0][0] == null)
215       return "0 0 0" + eol;
216     sb.appendI(f.length).append(" ")
217       .appendI(f[0].length).append(" ")
218       .appendI(f[0][0].length);
219     for (int i = 0; i < f.length; i++)
220       if (f[i] != null) {
221         sb.append(eol);
222         for (int j = 0; j < f[i].length; j++)
223           if (f[i][j] != null) {
224             sb.append(eol);
225             for (int k = 0; k < f[i][j].length; k++)
226               sb.appendF(f[i][j][k]).appendC('\t');
227           }
228       }
229     return sb.toString();
230   }
231 
232   /**
233    *
234    * @param list
235    *          list of strings to serialize
236    * @param nicely TODO
237    * @return serialized array
238    */
eAS(String[] list, boolean nicely)239   public static String eAS(String[] list, boolean nicely) {
240     if (list == null)
241       return PT.esc("");
242     SB s = new SB();
243     s.append("[");
244     for (int i = 0; i < list.length; i++) {
245       if (i > 0)
246         s.append(", ");
247       s.append(nicely ? escapeNice(list[i]) : PT.esc(list[i]));
248     }
249     s.append("]");
250     return s.toString();
251   }
252 
eAI(int[] ilist)253   public static String eAI(int[] ilist) {
254     if (ilist == null)
255       return PT.esc("");
256     SB s = new SB();
257     s.append("[");
258     for (int i = 0; i < ilist.length; i++) {
259       if (i > 0)
260         s.append(", ");
261       s.appendI(ilist[i]);
262     }
263     return s.append("]").toString();
264   }
265 
eAD(double[] dlist)266   public static String eAD(double[] dlist) {
267     // from isosurface area or volume calc
268     if (dlist == null)
269       return PT.esc("");
270     SB s = new SB();
271     s.append("[");
272     for (int i = 0; i < dlist.length; i++) {
273       if (i > 0)
274         s.append(", ");
275       s.appendD(dlist[i]);
276     }
277     return s.append("]").toString();
278   }
279 
eAF(float[] flist)280   public static String eAF(float[] flist) {
281     if (flist == null)
282       return PT.esc("");
283     SB s = new SB();
284     s.append("[");
285     for (int i = 0; i < flist.length; i++) {
286       if (i > 0)
287         s.append(", ");
288       s.appendF(flist[i]);
289     }
290     return s.append("]").toString();
291   }
292 
eAP(T3[] plist)293   public static String eAP(T3[] plist) {
294     if (plist == null)
295       return PT.esc("");
296     SB s = new SB();
297     s.append("[");
298     for (int i = 0; i < plist.length; i++) {
299       if (i > 0)
300         s.append(", ");
301       s.append(eP(plist[i]));
302     }
303     return s.append("]").toString();
304   }
305 
escapeNice(String s)306   private static String escapeNice(String s) {
307     if (s == null)
308       return "null";
309     float f = PT.parseFloatStrict(s);
310     return (Float.isNaN(f) ? PT.esc(s) : s);
311   }
312 
uABsM(String s)313   public static Object uABsM(String s) {
314     if (s.charAt(0) == '{')
315       return uP(s);
316     if ((isStringArray(s)
317         || s.startsWith("[{") && s.indexOf("[{") == s.lastIndexOf("[{"))
318         && s.indexOf(',') < 0 && s.indexOf('.') < 0 && s.indexOf('-') < 0)
319       return BS.unescape(s);
320     if (s.startsWith("[["))
321       return unescapeMatrix(s);
322     return s;
323   }
324 
isStringArray(String s)325   public static boolean isStringArray(String s) {
326     return s.startsWith("({") && s.lastIndexOf("({") == 0
327         && s.indexOf("})") == s.length() - 2;
328   }
uP(String strPoint)329   public static Object uP(String strPoint) {
330     if (strPoint == null || strPoint.length() == 0)
331       return strPoint;
332     String str = strPoint.replace('\n', ' ').trim();
333     if (str.charAt(0) != '{' || str.charAt(str.length() - 1) != '}')
334       return strPoint;
335     float[] points = new float[5];
336     int nPoints = 0;
337     str = str.substring(1, str.length() - 1);
338     int[] next = new int[1];
339     for (; nPoints < 5; nPoints++) {
340       points[nPoints] = PT.parseFloatNext(str, next);
341       if (Float.isNaN(points[nPoints])) {
342         if (next[0] >= str.length() || str.charAt(next[0]) != ',')
343           break;
344         next[0]++;
345         nPoints--;
346       }
347     }
348     if (nPoints == 3)
349       return P3.new3(points[0], points[1], points[2]);
350     if (nPoints == 4)
351       return P4.new4(points[0], points[1], points[2], points[3]);
352     return strPoint;
353   }
354 
unescapeMatrix(String strMatrix)355   public static Object unescapeMatrix(String strMatrix) {
356     if (strMatrix == null || strMatrix.length() == 0)
357       return strMatrix;
358     String str = strMatrix.replace('\n', ' ').trim();
359     if (str.lastIndexOf("[[") != 0 || str.indexOf("]]") != str.length() - 2)
360       return strMatrix;
361     float[] points = new float[16];
362     str = str.substring(2, str.length() - 2).replace('[',' ').replace(']',' ').replace(',',' ');
363     int[] next = new int[1];
364     int nPoints = 0;
365     for (; nPoints < 16; nPoints++) {
366       points[nPoints] = PT.parseFloatNext(str, next);
367       if (Float.isNaN(points[nPoints])) {
368         break;
369       }
370     }
371     if (!Float.isNaN(PT.parseFloatNext(str, next)))
372       return strMatrix; // overflow
373     if (nPoints == 9)
374       return M3.newA9(points);
375     if (nPoints == 16)
376       return M4.newA16(points);
377     return strMatrix;
378   }
379 /*
380   public static Object unescapeArray(String strArray) {
381     if (strArray == null || strArray.length() == 0)
382       return strArray;
383     String str = strArray.replace('\n', ' ').replace(',', ' ').trim();
384     if (str.lastIndexOf("[") != 0 || str.indexOf("]") != str.length() - 1)
385       return strArray;
386     float[] points = Parser.parseFloatArray(str);
387     for (int i = 0; i < points.length; i++)
388       if (Float.isNaN(points[i]))
389         return strArray;
390     return points;
391   }
392 */
eBS(BS bs)393   public static String eBS(BS bs) {
394     return BS.escape(bs, '(', ')');
395   }
396 
eBond(BS bs)397   public static String eBond(BS bs) {
398     return BS.escape(bs, '[', ']');
399   }
400 
401   /**
402    * Used only for getProperty("readable",...)
403    *
404    * @param name
405    * @param info
406    * @return tabular listing, with array types
407    */
toReadable(String name, Object info)408   public static String toReadable(String name, Object info) {
409     SB sb =new SB();
410     String sep = "";
411     if (info == null)
412       return "null";
413     if (PT.isNonStringPrimitive(info))
414       return packageReadable(name, null, info.toString());
415     if (info instanceof String)
416       return packageReadable(name, null, PT.esc((String) info));
417     if (info instanceof SV)
418       return packageReadable(name, null, ((SV) info).escape());
419     if (AU.isAS(info)) {
420       sb.append("[");
421       int imax = ((String[]) info).length;
422       for (int i = 0; i < imax; i++) {
423         // this fix, from PT.esc to toReadable, is not necessary
424         // for SwingJS
425         sb.append(sep).append(toReadable(null, ((String[]) info)[i]));
426         sep = ",";
427       }
428       sb.append("]");
429       return packageReadableSb(name, "String[" + imax + "]", sb);
430     }
431     if (AU.isAI(info)) {
432       sb.append("[");
433       int imax = ((int[]) info).length;
434       for (int i = 0; i < imax; i++) {
435         sb.append(sep).appendI(((int[]) info)[i]);
436         sep = ",";
437       }
438       sb.append("]");
439       return packageReadableSb(name, "int[" + imax + "]", sb);
440     }
441     if (AU.isAF(info)) {
442       sb.append("[");
443       int imax = ((float[]) info).length;
444       for (int i = 0; i < imax; i++) {
445         sb.append(sep).appendF(((float[]) info)[i]);
446         sep = ",";
447       }
448       sb.append("]");
449       return packageReadableSb(name, "float[" + imax + "]", sb);
450     }
451     if (AU.isAD(info)) {
452       sb.append("[");
453       int imax = ((double[]) info).length;
454       for (int i = 0; i < imax; i++) {
455         sb.append(sep).appendD(((double[]) info)[i]);
456         sep = ",";
457       }
458       sb.append("]");
459       return packageReadableSb(name, "double[" + imax + "]", sb);
460     }
461     if (AU.isAP(info)) {
462       sb.append("[");
463       int imax = ((T3[]) info).length;
464       for (int i = 0; i < imax; i++) {
465         sb.append(sep).append(eP(((T3[])info)[i]));
466         sep = ",";
467       }
468       sb.append("]");
469       return packageReadableSb(name, "point3f[" + imax + "]", sb);
470     }
471     if (AU.isASS(info)) {
472       sb.append("[");
473       int imax = ((String[][]) info).length;
474       for (int i = 0; i < imax; i++) {
475         sb.append(sep).append(toReadable(null, ((String[][]) info)[i]));
476         sep = ",\n";
477       }
478       sb.append("]");
479       return packageReadableSb(name, "String[" + imax + "][]", sb);
480     }
481     if (AU.isAII(info)) {
482       sb.append("[");
483       int imax = ((int[][]) info).length;
484       for (int i = 0; i < imax; i++) {
485         sb.append(sep).append(toReadable(null, ((int[][]) info)[i]));
486         sep = ",";
487       }
488       sb.append("]");
489       return packageReadableSb(name, "int[" + imax + "][]", sb);
490     }
491     if (AU.isAFF(info)) {
492       sb.append("[\n");
493       int imax = ((float[][]) info).length;
494       for (int i = 0; i < imax; i++) {
495         sb.append(sep).append(toReadable(null, ((float[][]) info)[i]));
496         sep = ",\n";
497       }
498       sb.append("]");
499       return packageReadableSb(name, "float[][]", sb);
500     }
501     if (AU.isADD(info)) {
502       sb.append("[\n");
503       int imax = ((double[][]) info).length;
504       for (int i = 0; i < imax; i++) {
505         sb.append(sep).append(toReadable(null, ((double[][]) info)[i]));
506         sep = ",\n";
507       }
508       sb.append("]");
509       return packageReadableSb(name, "double[][]", sb);
510     }
511     if (info instanceof Lst<?>) {
512       int imax = ((Lst<?>) info).size();
513       for (int i = 0; i < imax; i++) {
514         sb.append(toReadable(name + "[" + (i + 1) + "]", ((Lst<?>) info).get(i)));
515       }
516       return packageReadableSb(name, "List[" + imax + "]", sb);
517     }
518     if (info instanceof M34
519         || info instanceof T3
520         || info instanceof P4
521         || info instanceof A4) {
522       sb.append(e(info));
523       return packageReadableSb(name, null, sb);
524     }
525     if (info instanceof Map<?, ?>) {
526       Iterator<?> e = ((Map<?, ?>) info).keySet().iterator();
527       while (e.hasNext()) {
528         String key = (String) e.next();
529         sb.append(toReadable((name == null ? "" : name + ".") + key,
530             ((Map<?, ?>) info).get(key)));
531       }
532       return sb.toString();
533     }
534     return packageReadable(name, null, PT.toJSON(null, info));
535   }
536 
packageReadableSb(String infoName, String infoType, SB sb)537   private static String packageReadableSb(String infoName, String infoType,
538                                         SB sb) {
539     return packageReadable(infoName, infoType, sb.toString());
540   }
541 
packageReadable(String infoName, String infoType, String info)542   private static String packageReadable(String infoName, String infoType,
543                                         String info) {
544     String s = (infoType == null ? "" : infoType + "\t");
545     if (infoName == null)
546       return s + info;
547     return "\n" + infoName + "\t" + (infoType == null ? "" : "*" + infoType + "\t") + info;
548   }
549 
escapeModelFileNumber(int iv)550   public static String escapeModelFileNumber(int iv) {
551     return "" + (iv / 1000000) + "." + (iv % 1000000);
552   }
553 
encapsulateData(String name, Object data, int depth)554   public static String encapsulateData(String name, Object data, int depth) {
555     String s;
556     switch (depth) {
557     case JmolDataManager.DATA_TYPE_AF:
558       s = escapeFloatA((float[]) data, false) + ";\n";
559       break;
560     case JmolDataManager.DATA_TYPE_AFF:
561       s = escapeFloatAA((float[][]) data, true) + ";\n";
562       break;
563     case JmolDataManager.DATA_TYPE_AFFF:
564       s = escapeFloatAAA((float[][][]) data, true) + ";\n";
565       break;
566     default:
567       s = data.toString();
568       break;
569     }
570     return "  DATA \"" + name + "\"\n" + s + "    END \"" + name + "\";\n";
571 
572   }
573 
574 //  public static String escapeXml(Object value) {
575 //    if (value instanceof String)
576 //      return XmlUtil.wrapCdata(value.toString());
577 //    String s = "" + value;
578 //    if (s.length() == 0 || s.charAt(0) != '[')
579 //      return s;
580 //    return XmlUtil.wrapCdata(toReadable(null, value));
581 //  }
582 
unescapeUnicode(String s)583   public static String unescapeUnicode(String s) {
584     int ichMax = s.length();
585     SB sb = SB.newN(ichMax);
586     int ich = 0;
587     while (ich < ichMax) {
588       char ch = s.charAt(ich++);
589       if (ch == '\\' && ich < ichMax) {
590         ch = s.charAt(ich++);
591         switch (ch) {
592         case 'u':
593           if (ich < ichMax) {
594             int unicode = 0;
595             for (int k = 4; --k >= 0 && ich < ichMax;) {
596               char chT = s.charAt(ich);
597               int hexit = getHexitValue(chT);
598               if (hexit < 0)
599                 break;
600               unicode <<= 4;
601               unicode += hexit;
602               ++ich;
603             }
604             ch = (char) unicode;
605           }
606         }
607       }
608       sb.appendC(ch);
609     }
610     return sb.toString();
611   }
612 
getHexitValue(char ch)613   public static int getHexitValue(char ch) {
614     if (ch >= 48 && ch <= 57)
615       return ch - 48;
616     else if (ch >= 97 && ch <= 102)
617       return 10 + ch - 97;
618     else if (ch >= 65 && ch <= 70)
619       return 10 + ch - 65;
620     else
621       return -1;
622   }
623 
unescapeStringArray(String data)624   public static String[] unescapeStringArray(String data) {
625     // was only used for  LOAD "[\"...\",\"....\",...]" (coming from implicit string)
626     // now also used for simulation peaks array from JSpecView,
627     // which double-escapes strings, I guess
628     //TODO -- should recognize '..' as well as "..." ?
629     if (data == null || !data.startsWith("[") || !data.endsWith("]"))
630       return null;
631     Lst<String> v = new  Lst<String>();
632     int[] next = new int[1];
633     next[0] = 1;
634     while (next[0] < data.length()) {
635       String s = PT.getQuotedStringNext(data, next);
636       if (s == null)
637         return null;
638       v.addLast(PT.rep(s, "\\\"", "\""));
639       while (next[0] < data.length() && data.charAt(next[0]) != '"')
640         next[0]++;
641     }
642     return v.toArray(new String[v.size()]);
643   }
644 
isAV(Object x)645   public static boolean isAV(Object x) {
646     /**
647      * @j2sNative
648      *  return Clazz.instanceOf(x[0], org.jmol.script.SV);
649      */
650     {
651     return x instanceof SV[];
652     }
653   }
654 
655   /**
656    * Jmol-specific post-processing of
657    * the array data returned by Measure.computeHelicalAxis
658    *
659    * @param id
660    * @param tokType
661    * @param a
662    * @param b
663    * @param pts
664    * @return various objects depending upon tokType
665    */
escapeHelical(String id, int tokType, P3 a, P3 b, T3[] pts)666   public static Object escapeHelical(String id, int tokType, P3 a, P3 b, T3[] pts) {
667     // new T3[] { pt_a_prime, n, r, P3.new3(theta, pitch, residuesPerTurn), pt_b_prime };
668     switch (tokType) {
669     case T.point:
670       return (pts == null ? new P3() : pts[0]);
671     case T.axis:
672     case T.radius:
673       return (pts == null ? new V3() : pts[tokType == T.axis ? 1 : 2]);
674     case T.angle:
675       return Float.valueOf(pts == null ? Float.NaN : pts[3].x);
676     case T.draw:
677       return (pts == null ? "" : "draw ID \"" + id + "\" VECTOR "
678           + Escape.eP(pts[0]) + " " + Escape.eP(pts[1]) + " color "
679           + (pts[3].x < 0 ? "{255.0 200.0 0.0}" : "{255.0 0.0 128.0}"));
680     case T.measure:
681       return (pts == null ? "" : "measure " + Escape.eP(a) + Escape.eP(pts[0])
682           + Escape.eP(pts[4]))
683           + Escape.eP(b);
684     default:
685       return (pts == null ? new T3[0] : pts);
686     }
687   }
688 
689 }
690