OSDN Git Service

Wikipedia翻訳支援ツール Ver1.10時点のソース
[wptscs/wpts.git] / Wptscs / MainForm.cs
1 // ================================================================================================
2 // <summary>
3 //      Wikipedia翻訳支援ツール主画面クラスソース</summary>
4 //
5 // <copyright file="MainForm.cs" company="honeplusのメモ帳">
6 //      Copyright (C) 2012 Honeplus. All rights reserved.</copyright>
7 // <author>
8 //      Honeplus</author>
9 // ================================================================================================
10
11 namespace Honememo.Wptscs
12 {
13     using System;
14     using System.Collections.Generic;
15     using System.ComponentModel;
16     using System.Data;
17     using System.Drawing;
18     using System.IO;
19     using System.Text;
20     using System.Windows.Forms;
21     using Honememo.Utilities;
22     using Honememo.Wptscs.Logics;
23     using Honememo.Wptscs.Models;
24     using Honememo.Wptscs.Properties;
25     using Honememo.Wptscs.Utilities;
26     using Honememo.Wptscs.Websites;
27
28     /// <summary>
29     /// Wikipedia翻訳支援ツール主画面のクラスです。
30     /// </summary>
31     public partial class MainForm : Form
32     {
33         #region private変数
34
35         /// <summary>
36         /// 現在読み込んでいるアプリケーションの設定。
37         /// </summary>
38         private Config config;
39
40         /// <summary>
41         /// 検索支援処理クラスのオブジェクト。
42         /// </summary>
43         private Translator translator;
44
45         /// <summary>
46         /// 表示済みログ文字列長。
47         /// </summary>
48         private int logLength;
49
50         #endregion
51
52         #region コンストラクタ
53
54         /// <summary>
55         /// コンストラクタ。初期化メソッド呼び出しのみ。
56         /// </summary>
57         public MainForm()
58         {
59             // Windows フォーム デザイナで生成されたコード
60             this.InitializeComponent();
61         }
62
63         #endregion
64
65         #region 各イベントのメソッド
66
67         /// <summary>
68         /// フォームロード時の処理。初期化。
69         /// </summary>
70         /// <param name="sender">イベント発生オブジェクト。</param>
71         /// <param name="e">発生したイベント。</param>
72         private void MainForm_Load(object sender, EventArgs e)
73         {
74             // 設定ファイルの読み込み
75             if (!this.LoadConfig())
76             {
77                 // 読み込み失敗時はどうしようもないのでそのまま終了
78                 this.Close();
79             }
80
81             this.translator = null;
82             Control.CheckForIllegalCrossThreadCalls = false;
83
84             // コンボボックス設定
85             this.Initialize();
86
87             // 前回の処理状態を復元
88             this.textBoxSaveDirectory.Text = Settings.Default.SaveDirectory;
89             this.comboBoxSource.SelectedText = Settings.Default.LastSelectedSource;
90             this.comboBoxTarget.SelectedText = Settings.Default.LastSelectedTarget;
91
92             // コンボボックス変更時の処理をコール
93             this.ComboBoxSource_SelectedIndexChanged(sender, e);
94             this.ComboBoxTarget_SelectedIndexChanged(sender, e);
95         }
96
97         /// <summary>
98         /// フォームクローズ時の処理。処理状態を保存。
99         /// </summary>
100         /// <param name="sender">イベント発生オブジェクト。</param>
101         /// <param name="e">発生したイベント。</param>
102         private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
103         {
104             // 現在の作業フォルダ、絞込み文字列を保存
105             Settings.Default.SaveDirectory = this.textBoxSaveDirectory.Text;
106             Settings.Default.LastSelectedSource = this.comboBoxSource.Text;
107             Settings.Default.LastSelectedTarget = this.comboBoxTarget.Text;
108             Settings.Default.Save();
109         }
110
111         /// <summary>
112         /// 翻訳元コンボボックス変更時の処理。
113         /// </summary>
114         /// <param name="sender">イベント発生オブジェクト。</param>
115         /// <param name="e">発生したイベント。</param>
116         private void ComboBoxSource_SelectedIndexChanged(object sender, EventArgs e)
117         {
118             // ラベルに言語名を表示する
119             this.labelSource.Text = String.Empty;
120             this.linkLabelSourceURL.Text = "http://";
121             if (!String.IsNullOrWhiteSpace(this.comboBoxSource.Text))
122             {
123                 this.comboBoxSource.Text = this.comboBoxSource.Text.Trim().ToLower();
124
125                 // その言語の、ユーザーが使用している言語での表示名を表示
126                 // (日本語環境だったら日本語を、英語だったら英語を)
127                 Language.LanguageName name;
128                 this.labelSource.Text = String.Empty;
129                 if (this.config.GetWebsite(this.comboBoxSource.Text) != null &&
130                     this.config.GetWebsite(this.comboBoxSource.Text).Language.Names.TryGetValue(
131                     System.Globalization.CultureInfo.CurrentCulture.TwoLetterISOLanguageName,
132                     out name))
133                 {
134                     this.labelSource.Text = name.Name;
135                 }
136
137                 // サーバーURLの表示
138                 this.linkLabelSourceURL.Text = this.config.GetWebsite(
139                     this.comboBoxSource.Text).Location;
140             }
141         }
142
143         /// <summary>
144         /// 翻訳元コンボボックスフォーカス喪失時の処理。
145         /// </summary>
146         /// <param name="sender">イベント発生オブジェクト。</param>
147         /// <param name="e">発生したイベント。</param>
148         private void ComboBoxSource_Leave(object sender, EventArgs e)
149         {
150             // 直接入力された場合の対策、変更時の処理をコール
151             this.ComboBoxSource_SelectedIndexChanged(sender, e);
152         }
153
154         /// <summary>
155         /// リンクラベルのリンククリック時の処理。
156         /// </summary>
157         /// <param name="sender">イベント発生オブジェクト。</param>
158         /// <param name="e">発生したイベント。</param>
159         private void LinkLabelSourceURL_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
160         {
161             // リンクを開く
162             System.Diagnostics.Process.Start(((LinkLabel)sender).Text);
163         }
164
165         /// <summary>
166         /// 翻訳先コンボボックス変更時の処理。
167         /// </summary>
168         /// <param name="sender">イベント発生オブジェクト。</param>
169         /// <param name="e">発生したイベント。</param>
170         private void ComboBoxTarget_SelectedIndexChanged(object sender, EventArgs e)
171         {
172             // ラベルに言語名を表示する
173             this.labelTarget.Text = String.Empty;
174             if (!String.IsNullOrWhiteSpace(this.comboBoxTarget.Text))
175             {
176                 this.comboBoxTarget.Text = this.comboBoxTarget.Text.Trim().ToLower();
177
178                 // その言語の、ユーザーが使用している言語での表示名を表示
179                 // (日本語環境だったら日本語を、英語だったら英語を)
180                 if (this.config.GetWebsite(
181                     this.comboBoxTarget.Text) != null)
182                 {
183                     this.labelTarget.Text = this.config.GetWebsite(
184                         this.comboBoxTarget.Text).Language.Names[System.Globalization.CultureInfo.CurrentCulture.TwoLetterISOLanguageName].Name;
185                 }
186             }
187         }
188
189         /// <summary>
190         /// 翻訳先コンボボックスフォーカス喪失時の処理。
191         /// </summary>
192         /// <param name="sender">イベント発生オブジェクト。</param>
193         /// <param name="e">発生したイベント。</param>
194         private void ComboBoxTarget_Leave(object sender, EventArgs e)
195         {
196             // 直接入力された場合の対策、変更時の処理をコール
197             this.ComboBoxTarget_SelectedIndexChanged(sender, e);
198         }
199
200         /// <summary>
201         /// 設定ボタン押下時の処理。
202         /// </summary>
203         /// <param name="sender">イベント発生オブジェクト。</param>
204         /// <param name="e">発生したイベント。</param>
205         private void ButtonConfig_Click(object sender, EventArgs e)
206         {
207             // 設定画面を開く
208             ConfigForm form = new ConfigForm(this.config);
209             form.ShowDialog();
210
211             // 戻ってきたら設定ファイルを再読み込み
212             // ※ 万が一エラーでもとりあえず続行
213             this.LoadConfig();
214
215             // コンボボックス設定
216             string backupSourceSelected = this.comboBoxSource.SelectedText;
217             string backupSourceTarget = this.comboBoxTarget.SelectedText;
218             this.Initialize();
219             this.comboBoxSource.SelectedText = backupSourceSelected;
220             this.comboBoxTarget.SelectedText = backupSourceTarget;
221
222             // コンボボックス変更時の処理をコール
223             this.ComboBoxSource_SelectedIndexChanged(sender, e);
224             this.ComboBoxTarget_SelectedIndexChanged(sender, e);
225         }
226
227         /// <summary>
228         /// 参照ボタン押下時の処理。
229         /// </summary>
230         /// <param name="sender">イベント発生オブジェクト。</param>
231         /// <param name="e">発生したイベント。</param>
232         private void ButtonSaveDirectory_Click(object sender, EventArgs e)
233         {
234             // フォルダ名が入力されている場合、それを初期位置に設定
235             if (!String.IsNullOrEmpty(this.textBoxSaveDirectory.Text))
236             {
237                 this.folderBrowserDialogSaveDirectory.SelectedPath = this.textBoxSaveDirectory.Text;
238             }
239
240             // フォルダ選択画面をオープン
241             if (this.folderBrowserDialogSaveDirectory.ShowDialog() == System.Windows.Forms.DialogResult.OK)
242             {
243                 // フォルダが選択された場合、フォルダ名に選択されたフォルダを設定
244                 this.textBoxSaveDirectory.Text = this.folderBrowserDialogSaveDirectory.SelectedPath;
245             }
246         }
247
248         /// <summary>
249         /// 出力先テキストボックスフォーカス喪失時の処理。
250         /// </summary>
251         /// <param name="sender">イベント発生オブジェクト。</param>
252         /// <param name="e">発生したイベント。</param>
253         private void TextBoxSaveDirectory_Leave(object sender, EventArgs e)
254         {
255             // 空白を削除
256             this.textBoxSaveDirectory.Text = this.textBoxSaveDirectory.Text.Trim();
257         }
258
259         /// <summary>
260         /// 実行ボタン押下時の処理。
261         /// </summary>
262         /// <param name="sender">イベント発生オブジェクト。</param>
263         /// <param name="e">発生したイベント。</param>
264         private void ButtonRun_Click(object sender, EventArgs e)
265         {
266             // フォーム入力値をチェック
267             if (String.IsNullOrWhiteSpace(this.comboBoxSource.Text))
268             {
269                 FormUtils.WarningDialog(Resources.WarningMessageNotSelectedSource);
270                 this.comboBoxSource.Focus();
271                 return;
272             }
273             else if (String.IsNullOrWhiteSpace(this.comboBoxTarget.Text))
274             {
275                 FormUtils.WarningDialog(Resources.WarningMessageNotSelectedTarget);
276                 this.comboBoxTarget.Focus();
277                 return;
278             }
279             else if (!String.IsNullOrWhiteSpace(this.comboBoxSource.Text)
280                 && this.comboBoxSource.Text == this.comboBoxTarget.Text)
281             {
282                 FormUtils.WarningDialog(Resources.WarningMessageEqualsSourceAndTarget);
283                 this.comboBoxTarget.Focus();
284                 return;
285             }
286             else if (String.IsNullOrWhiteSpace(this.textBoxSaveDirectory.Text))
287             {
288                 FormUtils.WarningDialog(Resources.WarningMessageEmptySaveDirectory);
289                 this.textBoxSaveDirectory.Focus();
290                 return;
291             }
292             else if (!Directory.Exists(this.textBoxSaveDirectory.Text))
293             {
294                 FormUtils.WarningDialog(Resources.WarningMessageIgnoreSaveDirectory);
295                 this.textBoxSaveDirectory.Focus();
296                 return;
297             }
298             else if (String.IsNullOrWhiteSpace(this.textBoxArticle.Text))
299             {
300                 FormUtils.WarningDialog(Resources.WarningMessageEmptyArticle);
301                 this.textBoxArticle.Focus();
302                 return;
303             }
304
305             // 画面をロック
306             this.LockOperation();
307
308             // バックグラウンド処理を実行
309             this.backgroundWorkerRun.RunWorkerAsync();
310         }
311
312         /// <summary>
313         /// 中止ボタン押下時の処理。
314         /// </summary>
315         /// <param name="sender">イベント発生オブジェクト。</param>
316         /// <param name="e">発生したイベント。</param>
317         private void ButtonStop_Click(object sender, EventArgs e)
318         {
319             // 処理を中断
320             this.buttonStop.Enabled = false;
321             if (this.backgroundWorkerRun.IsBusy == true)
322             {
323                 System.Diagnostics.Debug.WriteLine("MainForm.-Stop_Click > 処理中断");
324                 this.backgroundWorkerRun.CancelAsync();
325                 if (this.translator != null)
326                 {
327                     this.translator.CancellationPending = true;
328                 }
329             }
330         }
331
332         /// <summary>
333         /// 実行ボタン バックグラウンド処理(スレッド)。
334         /// </summary>
335         /// <param name="sender">イベント発生オブジェクト。</param>
336         /// <param name="e">発生したイベント。</param>
337         private void BackgroundWorkerRun_DoWork(object sender, DoWorkEventArgs e)
338         {
339             try
340             {
341                 // 初期化と開始メッセージ
342                 this.textBoxLog.Clear();
343                 this.logLength = 0;
344                 this.textBoxLog.AppendText(String.Format(Resources.LogMessageStart, FormUtils.ApplicationName(), DateTime.Now.ToString("F")));
345
346                 // 翻訳支援処理ロジックのオブジェクトを生成
347                 try
348                 {
349                     this.translator = Translator.Create(this.config, this.comboBoxSource.Text, this.comboBoxTarget.Text);
350                 }
351                 catch (NotImplementedException)
352                 {
353                     // 設定ファイルに対応していないパターンが書かれている場合の例外、将来の拡張用
354                     this.textBoxLog.AppendText(String.Format(Resources.InformationMessageDevelopingMethod, "Wikipedia以外の処理"));
355                     FormUtils.InformationDialog(Resources.InformationMessageDevelopingMethod, "Wikipedia以外の処理");
356                     return;
357                 }
358
359                 // ログ・処理状態更新通知を受け取るためのイベント登録
360                 // 処理時間更新用にタイマーを起動
361                 this.translator.LogUpdate += new EventHandler(this.GetLogUpdate);
362                 this.translator.StatusUpdate += new EventHandler(this.GetStatusUpdate);
363                 this.Invoke((MethodInvoker)delegate { this.timerStatusStopwatch.Start(); });
364
365                 // 翻訳支援処理を実行
366                 bool success = true;
367                 try
368                 {
369                     this.translator.Run(this.textBoxArticle.Text.Trim());
370                 }
371                 catch (ApplicationException)
372                 {
373                     // 中止要求で停止した場合、その旨イベントに格納する
374                     e.Cancel = this.backgroundWorkerRun.CancellationPending;
375                     success = false;
376                 }
377                 finally
378                 {
379                     // 処理時間更新用のタイマーを終了
380                     this.Invoke((MethodInvoker)delegate { this.timerStatusStopwatch.Stop(); });
381                 }
382
383                 // 実行結果から、ログと変換後テキストをファイル出力
384                 this.WriteResult(success);
385             }
386             catch (Exception ex)
387             {
388                 this.textBoxLog.AppendText("\r\n" + String.Format(Resources.ErrorMessageDevelopmentError, ex.Message, ex.StackTrace) + "\r\n");
389             }
390         }
391
392         /// <summary>
393         /// 実行ボタン バックグラウンド処理(終了時)。
394         /// </summary>
395         /// <param name="sender">イベント発生オブジェクト。</param>
396         /// <param name="e">発生したイベント。</param>
397         private void BackgroundWorkerRun_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
398         {
399             // 設定ファイルのキャッシュ情報を更新
400             // ※ 微妙に時間がかかるので、ステータスバーに通知
401             try
402             {
403                 this.toolStripStatusLabelStatus.Text = Resources.StatusCacheUpdating;
404                 try
405                 {
406                     this.config.Save(Settings.Default.ConfigurationFile);
407                 }
408                 finally
409                 {
410                     this.toolStripStatusLabelStatus.Text = String.Empty;
411                 }
412             }
413             catch (Exception ex)
414             {
415                 FormUtils.WarningDialog(
416                     Resources.WarningMessageCacheSaveFailed,
417                     ex.Message);
418             }
419
420             // 画面をロック中から解放
421             this.Release();
422         }
423
424         /// <summary>
425         /// ステータスバー処理時間更新タイマー処理。
426         /// </summary>
427         /// <param name="sender">イベント発生オブジェクト。</param>
428         /// <param name="e">発生したイベント。</param>
429         private void TimerStatusStopwatch_Tick(object sender, EventArgs e)
430         {
431             // 処理時間をステータスバーに反映
432             this.toolStripStatusLabelStopwatch.Text = String.Format(Resources.ElapsedTime, this.translator.Stopwatch.Elapsed);
433         }
434
435         #endregion
436
437         #region それ以外のメソッド
438
439         /// <summary>
440         /// 画面初期化処理。
441         /// </summary>
442         private void Initialize()
443         {
444             // コンボボックス設定
445             this.comboBoxSource.Items.Clear();
446             this.comboBoxTarget.Items.Clear();
447
448             // 設定ファイルに存在する全言語を選択肢として登録する
449             foreach (Website site in this.config.Websites)
450             {
451                 this.comboBoxSource.Items.Add(site.Language.Code);
452                 this.comboBoxTarget.Items.Add(site.Language.Code);
453             }
454         }
455
456         /// <summary>
457         /// 設定ファイル読み込み。
458         /// </summary>
459         /// <returns>読み込み成功時は<c>true</c>。</returns>
460         private bool LoadConfig()
461         {
462             // 設定ファイルの読み込み
463             // ※ 微妙に時間がかかるので、ステータスバーに通知
464             try
465             {
466                 this.toolStripStatusLabelStatus.Text = Resources.StatusConfigReading;
467                 try
468                 {
469                     this.config = Config.GetInstance(Settings.Default.ConfigurationFile);
470                 }
471                 finally
472                 {
473                     this.toolStripStatusLabelStatus.Text = String.Empty;
474                 }
475             }
476             catch (FileNotFoundException ex)
477             {
478                 // 設定ファイルが見つからない場合
479                 System.Diagnostics.Debug.WriteLine(
480                     "MainForm.LoadConfig > 設定ファイル読み込み失敗 : " + ex.Message);
481                 FormUtils.ErrorDialog(
482                     Resources.ErrorMessageConfigNotFound,
483                     Settings.Default.ConfigurationFile);
484
485                 return false;
486             }
487             catch (Exception ex)
488             {
489                 System.Diagnostics.Debug.WriteLine(
490                     "MainForm.LoadConfig > 設定ファイル読み込み時エラー : " + ex.StackTrace);
491                 FormUtils.ErrorDialog(
492                     Resources.ErrorMessageConfigLordFailed,
493                     ex.Message);
494
495                 return false;
496             }
497
498             return true;
499         }
500
501         /// <summary>
502         /// 画面をロック中に移行。
503         /// </summary>
504         private void LockOperation()
505         {
506             // 各種ボタンなどを入力不可に変更
507             this.groupBoxTransfer.Enabled = false;
508             this.groupBoxSaveDirectory.Enabled = false;
509             this.textBoxArticle.Enabled = false;
510             this.buttonRun.Enabled = false;
511
512             // 中止ボタンを有効に変更
513             this.buttonStop.Enabled = true;
514         }
515
516         /// <summary>
517         /// 画面をロック中から解放。
518         /// </summary>
519         private void Release()
520         {
521             // 中止ボタンを入力不可に変更
522             this.buttonStop.Enabled = false;
523
524             // 各種ボタンなどを有効に変更
525             this.groupBoxTransfer.Enabled = true;
526             this.groupBoxSaveDirectory.Enabled = true;
527             this.textBoxArticle.Enabled = true;
528             this.buttonRun.Enabled = true;
529         }
530
531         /// <summary>
532         /// 翻訳支援処理のログ・変換後テキストをファイル出力。
533         /// </summary>
534         /// <param name="success">翻訳支援処理が成功した場合<c>true</c>。</param>
535         private void WriteResult(bool success)
536         {
537             // 若干時間がかかるのでステータスバーに通知
538             this.toolStripStatusLabelStatus.Text = Resources.StatusFileWriting;
539             try
540             {
541                 // 使用可能な出力ファイル名を生成
542                 string fileName;
543                 string logName;
544                 this.MakeFileName(out fileName, out logName, this.textBoxArticle.Text.Trim(), this.textBoxSaveDirectory.Text);
545
546                 if (success)
547                 {
548                     // 翻訳支援処理成功時は変換後テキストを出力
549                     try
550                     {
551                         File.WriteAllText(Path.Combine(this.textBoxSaveDirectory.Text, fileName), this.translator.Text);
552                         this.textBoxLog.AppendText(String.Format(Resources.LogMessageEnd, fileName, logName));
553                     }
554                     catch (Exception ex)
555                     {
556                         this.textBoxLog.AppendText(String.Format(Resources.LogMessageFileSaveFailed, Path.Combine(this.textBoxSaveDirectory.Text, fileName), ex.Message));
557                         this.textBoxLog.AppendText(String.Format(Resources.LogMessageStop, logName));
558                     }
559                 }
560                 else
561                 {
562                     this.textBoxLog.AppendText(String.Format(Resources.LogMessageStop, logName));
563                 }
564
565                 // ログを出力
566                 try
567                 {
568                     File.WriteAllText(Path.Combine(this.textBoxSaveDirectory.Text, logName), this.textBoxLog.Text);
569                 }
570                 catch (Exception ex)
571                 {
572                     this.textBoxLog.AppendText(String.Format(Resources.LogMessageFileSaveFailed, Path.Combine(this.textBoxSaveDirectory.Text, logName), ex.Message));
573                 }
574             }
575             finally
576             {
577                 // ステータスバーをクリア
578                 this.toolStripStatusLabelStatus.Text = String.Empty;
579             }
580         }
581
582         /// <summary>
583         /// 渡された文字列から.txtと.logの重複していないファイル名を作成。
584         /// </summary>
585         /// <param name="fileName">出力結果ファイル名。</param>
586         /// <param name="logName">出力ログファイル名。</param>
587         /// <param name="text">出力する結果テキスト。</param>
588         /// <param name="dir">出力先ディレクトリ。</param>
589         /// <returns><c>true</c> 出力成功</returns>
590         private bool MakeFileName(out string fileName, out string logName, string text, string dir)
591         {
592             // 出力先フォルダに存在しないファイル名(の拡張子より前)を作成
593             // ※渡されたWikipedia等の記事名にファイル名に使えない文字が含まれている場合、_ に置き換える
594             //   また、ファイル名が重複している場合、xx[0].txtのように連番を付ける
595             string fileNameBase = FormUtils.ReplaceInvalidFileNameChars(text);
596             fileName = fileNameBase + ".txt";
597             logName = fileNameBase + ".log";
598             bool success = false;
599             for (int i = 0; i < 100000; i++)
600             {
601                 // ※100000まで試して空きが見つからないことは無いはず、もし見つからなかったら最後のを上書き
602                 if (!File.Exists(Path.Combine(dir, fileName))
603                     && !File.Exists(Path.Combine(dir, logName)))
604                 {
605                     success = true;
606                     break;
607                 }
608
609                 fileName = fileNameBase + "[" + i + "]" + ".txt";
610                 logName = fileNameBase + "[" + i + "]" + ".log";
611             }
612
613             // 結果設定
614             return success;
615         }
616
617         /// <summary>
618         /// 翻訳支援処理クラスのログ更新イベント用。
619         /// </summary>
620         /// <param name="sender">イベント発生オブジェクト。</param>
621         /// <param name="e">発生したイベント。</param>
622         private void GetLogUpdate(object sender, EventArgs e)
623         {
624             // 前回以降に追加されたログをテキストボックスに出力
625             int length = this.translator.Log.Length;
626             if (length > this.logLength)
627             {
628                 this.textBoxLog.AppendText(this.translator.Log.Substring(this.logLength, length - this.logLength));
629             }
630
631             this.logLength = length;
632         }
633
634         /// <summary>
635         /// 翻訳支援処理クラスの処理状態更新イベント用。
636         /// </summary>
637         /// <param name="sender">イベント発生オブジェクト。</param>
638         /// <param name="e">発生したイベント。</param>
639         private void GetStatusUpdate(object sender, EventArgs e)
640         {
641             // 処理状態をステータスバーに通知
642             this.toolStripStatusLabelStatus.Text = this.translator.Status;
643         }
644
645         #endregion
646     }
647 }