ラベル 公開ソフトウェア の投稿を表示しています。 すべての投稿を表示
ラベル 公開ソフトウェア の投稿を表示しています。 すべての投稿を表示

2018年2月10日土曜日

英語論文執筆のために arXiv からの例文検索サービスを作った話

arXiv の論文から例文を検索する Hyper Collocation というサービスを公開しました.

以下はあまり整理されていない製作の記録です.

英語論文執筆用の例文検索サービス

英語での論文執筆の際に,専門用語を含む例文や言い回しのパターンを知りたいことが多々あります.有用なサービスとしては


がありますが,

  • データがライフサイエンス系の論文に限られている(ライフサイエンス辞書)
  • ソートの基準が頻度順ではないため典型的な例文が上位にこない
  • ストップワードに近い頻出語を検索した際の
    • 検索が重い(Springer Exemplar)
    • 表示可能な検索結果が偏る(ライフサイエンス辞書)

という不満点があったので,並行して個人的な資料から検索を行うプログラムを作って使っていました.

しかし,個人的に収集した英語論文ではやはりデータベースが小さく,例文検索エンジンとしてはいまいちだなーと思っていました.

arXiv Bulk Data Access

何がきっかけだったか忘れましたが,去年の年末あたりに arXiv が Bulk Data Access という全論文のデータを一括ダウンロードできるサービスを提供していることに気が付きました.

データの公開は Amazon S3 で  Requester Pays Buckets として行われており,700GBくらいのデータなので,全部ダウンロードすると8000円くらいかかります(多分それくらい請求されていたと思います).
これをダウンロードして例文検索エンジンを作れば冬休みの間楽しめそうだなと思ったので,今回の冬休みのPCと人間の空き時間をこの課題に投入することにしました.ちなみにダウンロード可能なデータには
  • PDF
  • LaTeXソース+図表のファイルのアーカイブ
の二種類がありますが,PDFよりも精度の良い文章の抽出が可能なのでLaTeXの方をダウンロードしました.

コーパスへの変換

ダウンロードしてきたデータはLaTeXのソースを含んだアーカイブなので,まずこれを展開して文章を抽出し,コーパスとして出力するプログラムを書く必要があります.LaTeXからプレーンテキストへの変換方法にはざっと調べた感じ
といった選択肢がありましたが,もとのデータ量がそこそこ多く,ヘッダ,図表,参考文献リスト,箇条書きなどの,そもそもコーパスに入れたくない情報はカットしておきたかったので,構文解析後のAST(抽象構文木)にフィルターを掛けられる Pandoc で変換を行うことにしました.

Pythonで書いたスクリプトですべてのファイルを処理し,得られたテキストデータのコーパスのサイズは約24GBでした.

FM-index に基づく検索エンジンの作成

検索のたびに24GBを普通に総当たりすると耐え難い速度になりますので,
検索用のインデックスを作成する必要があります.

10年ほど前に小規模なデータで同じことをやった時には接尾辞配列を使ったのですが,ざっくり元データの5倍程度のサイズのインデックスが必要で,24GBのコーパスを使うと100GB以上のインデックスをメモリ上に置くことになります.SSD上にインデックスを置くことも考えたのですが,雑な実装では耐え難い遅さだったので,データ構造の改善を考えることにしました.
本筋とは全く関係ないですが,このあたりまでは新年なので普段使わない言語を使おうと思い,C言語なみのパフォーマンスが出せそうな言語という基準で Go言語を使って書いていました.しかしWindows環境で32GB以上のメモリを使えないという仕様にげんなりしたので,やめにしました.代替として Rust を使おうかとも思ったのですが,残り時間が心もとなかったので,久しぶりにC++で書くことにしました.C++11以降の C++ は以前とはまるで別言語のようで,新しい言語を学ぶのと同程度に楽しかったです.
データ構造についての話にもどります.圧縮接尾辞配列を使えば元のデータよりも小さいインデックスで検索が可能という話を小耳に挟んでいたので,これについて勉強することにしました.このあたりの話題については,ネット上にわかりやすい資料がたくさんあって助かりました.

一通り調べて勉強した結果,今現在では FM-index というのを使うのがベストで実装としては Succinct Data Structure Library を使うのが楽ちんという結論に達しました.
WSL由来のiostreamの実装が4GB以上のファイルを読むと死ぬ問題など,色々と大きいデータに絡む問題がこの後も続きましたが,24GBのコーパスを5GB程度のインデックスに圧縮でき,スニペット200個を1秒程度で返せる速さになったので,この辺で満足してつぎの段階に進むことにしました.

Crow + Vue.js によるWebフロントエンド

Webブラウザから検索を行えるようにしたかったので,まず,軽量なC++用のフレームワークである Crow を利用し,REST API の形で検索を提供するアプリケーションサーバーを実装しました.Crowは今どきのC++の機能を活用した使いやすいフレームワークで,Python における Flask と同じような簡単さで書くことができます.

適当なテンプレートエンジンを持ってきてC++のアプリケーションサーバーに HTTP/HTTPS アクセスを受けさせようかとも思いましたが,

  • 静的なファイルを扱うのが面倒
  • SSL対応も面倒
  • 通信圧縮とかも面倒
  • リダイレクトとかしたくなった時も面倒
と,やっぱり普通のWebサーバーを立てておき,検索APIへのアクセスだけリバースプロキシでアプリケーションサーバに転送するのが妥当に思われたので,そうすることにしました.

Viewの部分には最近流行っていて使うのが簡単そうな Vue.js を使うことにしました.
5年ほど前に一度 AngularJS を使った時にも使いやすさに感動しましたが,Vue.js の方がすんなり使えたような気がします.とはいえ,どちらのフレームワークについても大したことをやらなかったので,明確な差を感じる部分はなかったです.

Vue.jsで作ったフロントエンドを含む静的なコンテンツの提供とリバースプロキシを行うための Webサーバーが必要ですので,ここには nginx を使うことにしました.
いつも Apache ばかりで nginx は初めて使いましたが,やっぱり設定ファイルが簡単なのはいいことですね.

Amazon Web Service へのデプロイ

C++で書かれたアプリケーションサーバーには約10GB (順方向と逆方向のインデックス5GB×2)のメモリが必要です.また,起動時に10GBのデータをHDDなりSSDなりから読み込む必要があるので,普段使いのPCで使うたびに起動するには起動時間が長すぎ,常駐させるにはメモリを食いすぎるという微妙な状況になってしまいました.

というわけで,検索エンジンをサーバーで走らせようと思い立ち,話にはよく聞くもののまともに使ったことがなかった Amazon Web Service (AWS) の EC2 を使ってみることにしました.
スペックを眺めてみたところ r4.large というインスタンスが必要最小限のスペックを持つようだったのでこのインスタンスを作成してデプロイしました.設定も簡単で速く,世間でAWSがもてはやされている理由の一端を体感できたように思います.

サービス公開

というわけで,快適な例文検索環境を手に入れることに成功したのですが,EC2 のr4.large インスタンスには月々1万円程度の維持費がかかり,個人的に使うにはちょっと基礎代謝が大きすぎるサービスになってしまいました.

個人所蔵の論文集をコーパスとしていた当時は,検索エンジンが合法かどうか議論されているような時代だったので,サービスの公開はあまり考えませんでしたが,
現時点では検索結果としてarXivにリンクする形でスニペットを表示することは問題ないようです.そこで,サービスを公開して大人数で有効活用することで元を取ることにしましたという次第です.

TeXclipでの経験に基づくと,広告でサーバー代が賄えることはあり得なさそうですが,公開することで得られる経験も色々とあったので,今回も元が取れた気分になるんじゃないかな~と期待しています.

感想

検索アルゴリズムやデータ構造にはプログラミングやコンピュータサイエンスのおもしろい要素が詰まっているので,たまに手をだすとおもしろいです.
あと,暫く見ないうちにC++がモダンな感じになっていてびっくりしました.つぎはEigenあたりを使いつつ勉強してみようかなという気になりました.

おまけ:名前の由来

冒頭の方でチラッと,今回公開した例文検索サービス以前から個人用のツールを作って使っていたことを書きました.サービス名の Hyper Collocation は10年ほど前から存在するこのツールから引き継いだものです.この名前は
  • 開発の初期段階で Hyper Estraier を参考にしたこと
  • 当時の先生に 新編 英和活用大辞典―英語を書くための38万例 を教えてもらい,
    大変役にたったことが開発のきっかけとなったこと
    (この辞書の表紙に THE KENKYUSHA DICTIONARY OF ENGLISH COLLOCATIONS と書いてあり,これを見て初めて collocation の概念を知りました.)
  • 当時は Web2.0 末期(DoCoMo2.0 がコケたころ)で,ハイパー**というネーミングが相当に使い古された感があったので,Hyper Collocation という名前をつけても名前被りの何かが出現する危険がなさそうに思われたこと
から適当に付けたような気がします.今となっては何もかも過去のことであり,当時のコードは一行も残っていませんが,多少の愛着があったので名前だけは残すことにしました.

参考文献

検索アルゴリズム

C++

2015年12月25日金曜日

TeXclipで作成した数式を後で編集する方法

TeXclipで作成した数式を修正する必要があるとき,再度TeXのコードを入力するのが面倒だという要望を頻繁にお受けしますので,簡単な方法を以下で説明します.

TeXclipの画像には代替テキストとしてTeXのソースコードを指定しています.
したがって,普通にPowerPointにコピペした画像については,「図の書式設定」の「代替テキスト」のところからTeXのソースコードを回収することが可能です.

以下のスクリーンショットは PowerPoint 2016 のものですが,リボンインターフェースに変わる前のバージョン(2003くらい?)でも「図の書式設定」ダイアログで同様の情報を取得できたはずです.「図の書式設定」はバージョンによらず画像を右クリックすると表示されるコンテキストメニューから表示できると思います.


フォントや解像度の設定についてはお手数ですが手作業で復元してください.

2015年11月18日水曜日

Kindle版の発売日を推定するためのWebサービスを作りました

Kindle版が紙媒体の書籍からどれくらいの時間差で発売されるかは,特に出版社ごとに決まった法則があるわけでもなく,本によってケースバイケースのようです.

しかし,漫画のようにコンスタントに続巻が発売されるものでは,発売日を順番に並べてみるとそれなりに規則性があり,十分に発売日を予測可能なように思われます.

というわけで,Kindle版・コミック版などの発売日を検索し,並べて表示するサイトを作りました.

刊行ペース検索
Kindle版に限らず発売日の予測に便利なので,ぜひご利用ください

以下は技術的なメモです.

2011年9月27日火曜日

HTML5で学会タイマー


※ いくつかアップデートしました (2015/10/03)
 ・URLに設定が反映されるようになりました.
  ブックマークすることで現在の設定を保存できます.
  (Chromeでローカルファイルからの実行の場合セキュリティ上の理由でアプリからURLを更新することができません.左上のロゴが現在の設定を含んだURLへのリンクになっていますので,こちらをご利用ください.)
 ・画面サイズに適応した表示の調整を強力にしました.

※ いくつかアップデートしました (2015/09/01)
 ・ 時間表示下部のコメントを編集可能にしました.
 ・ iOS対応しました.
 ・ ダウンロード先などを
GitHubに統一しました.

第40回制御理論シンポジウム で初めて学会の司会をする機会がありました.
この学会では司会者がタイムキーパーを兼ねるのですが,
緊張でベルを忘れる気がしたのでタイムキーパー業務を自動化することにしました.

世の中には結構な数の学会タイマーが出回っていますが,
  • 誤差が蓄積するソフトがある
  • カスタマイズが難しい
という話を聞いたので,今後のために勉強を兼ねて新たにタイマーを作ることにしました.

できたものを以下においておきます.
特長は
  • 直観的な操作
  • インストール不要・ダウンロードも可能
  • 緩いオープンソースライセンスなのでいつまでも好きなように使える
  • 設定がURLに格納されるので,ブックマークなどのブラウザ機能で
    設定を柔軟かつ簡単に管理できる
  • HTML5対応のブラウザがあれば環境を問わず動作する(mac や Linuxでも動作すると思います.iOSで音がならなかったようなので修正しました.)
といった感じです.
ブラウザを全画面化し,発表者の方に向けておくと発表中も時間がわかって安心です.
スクリーンセーバー等の動作にはご注意ください.

なお,zipファイル中に含まれている jquery-1.6.4.min.js (http://jquery.com/) および jquery.timer.js (http://mattptr.net/license/license.txt) はBSDライセンスで配布されているソフトウェアであり, bootstrap.min.css (http://twitter.github.com/bootstrap/) は Apache License v2.0 で配布されているソフトウェアです.詳しい情報は各ソフトウェアの配布元をご覧ください.
また,これらの有用なソフトウェアを公開されている方々に感謝します.

その他の部分について,このソフトウェアを使用・改変することは自由ですが,
完全な無保証ですので自己責任でご利用ください.

2010年10月26日火曜日

RenameSheet - Excelでファイル名一括変更

ファイル名を Excel のシート上で操作してリネームすることが出来ます.Excel のスプレッドシートなので
  • 連番を振る
  • 連番を逆にする
  • 作成日などをファイル名に加える
  • 作成した順に連番を振る
  • etc...
といった操作がいとも簡単にできることが特長です.下は連番の形式を変更している動画です.




次の動画はファイルの最終更新日を元にファイル名をリネームしている様子です.




このように高度な規則に基づいたリネームも Excel を使えば楽々です.

  • 逆再生動画の作成
  • デジカメ写真などの整理・連番化・日付によるラベリング

等で需要がある気がしたので一応ダウンロードできるようにしておきます.

このソフトは Excel 2010 のマクロ(VBA)で作ってあります.Excel でマクロを書くのは初めてなのでよくわかりませんが,他環境では動作しないかもしれません.
また,このソフトの使用によって生じた結果に関して一切の責任を負うことは出来ません.一応ファイルを操作するソフトなので,万全のバックアップ・テストを行い,自己責任でご利用ください.

反応できるかはわかりませんが,要望質問などがありましたらコメント欄かメールでお願いいたします.

2010年6月20日日曜日

英語プレゼン練習支援ツール

国際会議等で英語の発表をすることがしばしばあります.
僕は英語が苦手なので,毎回台本を作って練習していますが,発音をいちいち辞書引いて調べたり覚えたりするのが大変なので,自分用に英語プレゼンテーション練習支援ツールを作りました.
興味のある人は使ってみてください.

ppt2ipod


機能
  • PowerPoint のノート部分を Windows の音声合成機能で読み上げた wav ファイルを出力します.発音の参考になります.
  • PowerPoint のスライドを画像形式で出力します.iPodや携帯に入れておくと,どこでも発表の練習が出来ます.
  • ノート部分をテキストファイルに出力します.Mac で音声合成したい人向けです.

スクリーンショット



システムの必要用件


  • .NET Framework 4.0
    大半の人は未インストールのはずなので Microsoft のページからインストールしてください.「.NET Framework 4 Client Profile」が最低限必要です.
  • 英語がしゃべれる音声合成エンジン
    Windows 7 / Vista には標準で入ってました.XPは未確認ですが,7 / Vistaより流暢さで劣るものの入っているらしいです.
  • PowerPoint
    対象となるプレゼンテーションファイルを開けるPowerPointがインストールされている必要があります. PowerPoint 2007 / 2010 で動作確認しています.

ダウンロード
  • ppt2ipod.exe
    実行ファイルです.インストール不要なので,適当な場所に置いて実行してください.
  • test.ppt
    とりあえず試してみたいけど,台本付きの PowerPoint ファイルを作るのが面倒な人のために置いておきます.

その他コメント

  • 出力されたスライド画像とスピーチを iPod と同期しておくと電車や飛行機の中での練習・暗記に便利です.僕は iPod touch に入れてますが,スライド画像の表示が速いので不意に研究の説明を求められたときなんかにも便利です.
  • Mac の音声合成のほうが流暢な気がするので,Mac持ってる人はそっちで音声合成した方が良いとおもいます.その際に便利なテキストファイルもscriptフォルダに出力しています.
  • このソフトは無保証です.利用は自由ですが自己責任でお願いします.
  • ご意見・ご要望・質問などはこの記事にコメントしていただけると助かります.

2010年2月6日土曜日

MATLAB が出力する図を PowerPoint 向けに自動調節

MATLAB で描いた図やグラフを PowerPoint に貼り付けるときに
一番簡単な方法はFigure のメニューから [編集]-[Figureのコピー] を使って
PowerPoint 上でペーストすることだと思います.

しかし, MATLAB がデフォルト設定で作成するグラフには
以下のような問題があります.

  • プレゼン用には字が小さい
  • グリッドが出ない
    • 無いよりある方が無難だと思うのだけど・・・
  • コピー&ペースト時にサイズが変わる
    • グリッドまで変えられるので意図しない結果になることが多いですね
  • 線が見にくい
    • 細い&プロジェクターで見辛い色が使われます

丁寧に設定すればこれらの問題を解決することは可能ですが,
そこそこ面倒ですし,ミーティングなどで MATLAB による実演を行う際に
これらを逐一設定するのはテンポを損ねます.

というわけで,これらの設定を自動化するプログラムを作りました.
このプログラムは MATLAB のデフォルト設定を書き換えて,
以降に描かれるグラフをプレゼン用に見やすくします.

いまのところ MATLAB を再起動する以外に元に戻す方法はありません.

ちなみに得られるグラフは以下のようになります.

使用前


使用後
コード
function [] = prettify_figure_presen()
%PRETTIFY_FIGURE_PRESEN グラフのフォントをプレゼン用に最適化
% この関数を呼び出すと
% 以降に描かれるプロットの見た目がプレゼン向けになります.
% 元に戻すにはMATLABを再起動するしかないです.
%
% ※ ボード線図は別口(prettify_bodeplot_presen参照)
%    なので注意してください.


%% フォント
set(0,'defaultAxesFontSize',20);
set(0,'defaultAxesFontName','メイリオ');
set(0,'defaultAxesFontWeight','demi'); % normal/demi/bold
set(0,'defaultTextFontSize',20);
set(0,'defaultTextFontName','メイリオ');
set(0,'defaultTextFontWeight','demi');

%% 線の幅
set(0,'defaultAxesLineWidth', 1.5); % 軸
set(0,'defaultLineLineWidth', 2); % プロット

%% プロットの色
clorder = ...
    [  0,   0, 255; % 青
       0, 128,   0; % 緑
     255,   0,   0; % 赤
     204,   8, 204; % 紫
     222, 125,   0; % 茶
       0,  51, 153; % 紺 (青と区別しづらい,注意!)
      64,  64,  64];% 濃い灰色  
set(0,'defaultAxesColorOrder',clorder/255);

%% 最初からgrid on & box on
set(0,'DefaultAxesXGrid','on');
set(0,'DefaultAxesYGrid','on');
set(0,'DefaultAxesBox','on');

%% コピペ時にサイズを変更しない
set(0,'DefaultFigurePaperPositionMode','auto');

end

2010/2/17 追記
17行目の間違いを修正しました&reset(0)で設定を初期化できるそうです.
atsu-kan さんありがとうございます.

2009年5月26日火曜日

Arduinoで簡易オシロスコープ

2013/4/22 ライセンスを修正BSDライセンスとしました
Arduinoで簡単に使える簡易オシロスコープもどきを作ってみました.ちょっとした実験にはなかなか便利なので公開しておきます.

スクリーンショット

全体図scr1
マウスで周波数などの読み取りも可能
scr2

操作法

  • [↑][↓] 時間軸の変更
  • [space] データ取得の停止/再開
  • [s] データをcsv形式で保存
  • [マウス操作] 値の読み取り・ドラッグで周波数読み取り

インストール

  • 下記のソースの末尾・Arduino用のプログラムの部分をArduinoに書き込んでおく.
  • 下記のソースをProcessingにコピーし,シリアルポートの設定とフォントの生成/設定を行う.
  • 実行してみる

ソース


// ArduinoScope v1.0.0
// 
// Note:
//  1. please confirm serial port setting
//  2. prepare font
//  3. Arduino code is attached at the end of this code

// Copyright (c) 2009-2013 I. Maruta <ichiro.maruta@gmail.com>
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
// 2. Neither the name of the author nor the names of its contributors
//    may be used to endorse or promote products derived from this software
//    without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.

 

import processing.serial.*;

Serial ArduinoPort;  // Create object from Serial class

int NumOfScopes,NumOfInput=2;
int data_span=10000;
Strage dfs = new Strage();
Scope[] sp;

int fontsize=16;
PFont myFont;

void setup() 
{
  // Serial Port 
  println(Serial.list());
  String portName = Serial.list()[0]; // TODO: automatic detection?
  ArduinoPort = new Serial(this, portName, 38400);
  ArduinoPort.bufferUntil(10);

  // Screen
  size(800, 600);
  NumOfScopes=2;
  sp = new Scope[NumOfScopes];
  sp[0]= new Scope(0,50,10,width-100,height/2-35,512,-512,1000);
  sp[1]= new Scope(1,50,height/2+15,width-100,height/2-35,1024,0,1000);

  myFont = loadFont("Dotum-16.vlw");
  textFont(myFont,fontsize);
}

class Scope{
  int input_id;    // corresponding input
  int posx,posy;   // screen position of the scope
  int sizex,sizey; // pixel size of the scope
  float yu,yl;     // range of y is [yl,yu]
  int tspan;       //
  int ngx,ngy; // number of grids
  float maxposx,maxposy,minposx,minposy,maxx,minx,maxy,miny;

  Scope(int did,int px,int py,int sx,int sy,float syu,float syl,int ts){
    input_id=did;
    posx=px; 
    posy=py;
    sizex=sx; 
    sizey=sy;
    yu=syu; 
    yl=syl;
    tspan=ts;
    ngx=10; 
    ngy=4;
  }

  void grid(){
    pushStyle();
    fill(255,196);
    stroke(0,0,150);
    for(float gx=sizex; gx>=0; gx-= (float)sizex/ngx){
      line(posx+gx,posy,posx+gx,posy+sizey);
      textAlign(CENTER,TOP);
      text((int)map(gx,sizex,0,0,-tspan),posx+gx,posy+sizey+2);    
    }
    for(float gy=sizey; gy>=0; gy-= (float)sizey/ngy){
      line(posx,posy+gy,posx+sizex,posy+gy);
      textAlign(RIGHT,CENTER);
      text((int)map(gy,0,sizey,yu,yl),posx,posy+gy);    
    }
    popStyle(); 
  }

  int curx,cury;

  // draw cursor
  void cur()
  {
    // return if mouse cursor is not in this scope
    if(constrain(mouseX,posx,posx+sizex)!=mouseX 
      || constrain(mouseY,posy,posy+sizey)!=mouseY) return;

    pushStyle();

    // draw cross cursor
    stroke(255,0,0,196);
    fill(255,0,0,196);
    line(mouseX,posy,mouseX,posy+sizey);
    line(posx,mouseY,posx+sizex,mouseY);

    // draw measure if mouse is dragged
    if(mousePressed){
      line(curx,posy,curx,posy+sizey);
      line(posx,cury,posx+sizex,cury);
      textAlign(RIGHT,BOTTOM);
      text((int)map(curx,posx,posx+sizex,-tspan,0)+"ms, "+(int)map(cury,posy,posy+sizey,yu,yl),curx,cury);
      textAlign(LEFT,TOP); 
      text("("+nfp((int)map(mouseX-curx,0,sizex,0,tspan),1)+"ms, "+nfp((int)map(mouseY-cury,0,sizey,0,-(yu-yl)),1)+")\n"+nf(1000/map(mouseX-curx,0,sizex,0,tspan),1,2)+"Hz\n"+nf(TWO_PI*1000/map(mouseX-curx,0,sizex,0,tspan),1,2)+"rad/sec",mouseX,mouseY+2);
    }
    else{
      curx=mouseX;
      cury=mouseY;
      textAlign(RIGHT,BOTTOM);
      text((int)map(curx,posx,posx+sizex,-tspan,0)+"ms, "+(int)map(cury,posy,posy+sizey,yu,yl),curx,cury);
    }
    popStyle();
  }

  // draw min&max tick
  void minmax(){
    pushStyle();
    fill(255,128);
    stroke(0,0,100);
    textAlign(RIGHT,CENTER);
    line(posx,maxposy,posx+sizex,maxposy);
    text((int)maxy,posx,maxposy); 
    line(posx,minposy,posx+sizex,minposy);
    text((int)miny,posx,minposy); 
    textAlign(LEFT,CENTER);
    textAlign(CENTER,TOP);
    text("max",maxposx,maxposy); 
    textAlign(CENTER,BOTTOM);
    text("min",minposx,minposy+20); 
    popStyle();
  }

  // draw scope
  void Plot(){
    float sx,sy,ex,ey;
    int nof=0;
    DataFrame df_last = dfs.get(0);

    maxy=-1e10; // -inf
    miny=1e10;  // +inf

    // draw background (for transparency)
    pushStyle();
    noStroke();
    fill(0,0,64,64);
    rect(posx,posy,sizex,sizey);
    popStyle();

    // draw data plot
    pushStyle();
    stroke(0,255,0);
    smooth();
    strokeWeight(1);
    for(int idx=0;(dfs.get(idx).t>max(df_last.t-tspan,0)) && -idx<data_span;idx--){
      DataFrame df_new=dfs.get(idx);
      DataFrame df_old=dfs.get(idx-1);
      sx=(float) map(df_new.t, df_last.t, df_last.t - tspan, posx+sizex,posx);
      ex=(float) map(df_old.t, df_last.t, df_last.t - tspan, posx+sizex,posx);
      sy=(float) map((float)df_new.v[input_id],(float) yu,(float) yl,(float) posy,(float) posy+sizey );
      ey=(float) map((float)df_old.v[input_id],(float) yu,(float) yl,(float) posy,(float) posy+sizey );
      if(ex<posx){
        ey+=(sy-ey)*(posx-ex)/(sx-ex);
        ex=posx;
      }
      line(sx,sy,ex,ey);
      maxy=max(maxy,df_new.v[input_id]);
      if(maxy==df_new.v[input_id]){
        maxposx=sx;
        maxposy=sy;
      }
      miny=min(miny,df_new.v[input_id]);
      if(miny==df_new.v[input_id]){
        minposx=sx;
        minposy=sy;
      }
      nof++;
    }
    popStyle();

    //    minmax();    

    // draw current value of input
    pushStyle();
    textAlign(LEFT,CENTER);
    stroke(0,0,64);
    fill(0,255,0,196);
    text(df_last.v[input_id],posx+sizex,map(df_last.v[input_id], yu, yl, posy, posy+sizey ));
    popStyle();   

    grid();
    cur();    
  }
}

void draw() 
{ 
  background(0);

  for(int i=0;i<NumOfScopes;i++){
    sp[i].Plot();
  }
}

// input data buffer class
// (now using ring buffer)
class Strage{
  int cur;
  DataFrame[] DataFrames;

  Strage(){
    cur=0;
    DataFrames=new DataFrame[data_span];
    for(int idx=0;idx<data_span;idx++){
      int ret_v[] = new int[NumOfInput];
      DataFrames[idx] = new DataFrame(0,ret_v);
    }
  }

  void push(DataFrame d){
    cur = ((cur+1) %data_span);
    DataFrames[cur]=d;
  }

  DataFrame get(int idx)
  {
    int num=(cur+idx);
    for(; num<0; num+= data_span);
    return((DataFrame) DataFrames[num]);
  }

  void save()
  {
    String savePath = selectOutput();  // Opens file chooser
    if (savePath == null) {
      // If a file was not selected
      println("No output file was selected...");
    }else{
      PrintWriter output;
      output = createWriter(savePath); 
      DataFrame df_last = this.get(0);
      for(int idx=0;-idx<data_span;idx--){
        if(this.get(idx).t==0) break;
        output.print(this.get(idx).t-df_last.t);
        for(int k=0;k<NumOfInput;k++){
          output.print(","+this.get(idx).v[k]);
        } 
        output.println("");
      }
      output.flush();
      output.close();
    }
  }
}

class DataFrame{
  int t;
  int[] v;
  DataFrame(int st, int[] sv){
    t=st;
    v=sv.clone();
  }
}

boolean isactive=true;

// buffering data from serial port
void serialEvent(Serial myPort)
{
  int[] vals=new int[NumOfInput];
  int timestamp;
  int[] splitdata;
  if( myPort.available() > 0) { 
    String datline=myPort.readString();
    splitdata=parseInt(datline.split(","));
    if((splitdata.length==NumOfInput+2)){
      timestamp=splitdata[0];
      for(int idx=0;idx<NumOfInput;idx++){
        vals[idx]=splitdata[idx+1];
      }
      if(isactive){
        if((timestamp-dfs.get(0).t)<0){
          dfs.cur--;
        }
        if((timestamp-dfs.get(0).t) > ((float)sp[0].tspan / sp[0].sizex/2.0) ){
          dfs.push( new DataFrame(timestamp,vals));
        }
      }
    }
  }
}

// keyboard user interface
void keyPressed(){
  switch(key){
    // activate/deactivate scope update
  case ' ':
    isactive=!isactive;
    break;
    // save record
  case 's':
    dfs.save();
    break;
  case CODED:
    switch(keyCode){
      // Increse time span
    case UP:
      for(int i=0;i<NumOfScopes;i++){
        sp[i].tspan*=2;
      }
      break;
      // Decrease time span
    case DOWN:
      for(int i=0;i<NumOfScopes;i++){
        sp[i].tspan/=2;
      }
      break;
    }
    break;
  }
}

/* Arduino code
void setup()
{
  Serial.begin(38400);  
}

void loop()
{
  Serial.print(millis());
  Serial.print(",");
  Serial.print(analogRead(0));
  Serial.print(",");
  Serial.print(analogRead(1));
  Serial.println(",");
}
*/