nodelintでflymake

node-jslintでflymake - すぎゃーんメモ で設定した、node-jslintによるflymakeを使っていたのだけど、

var util = require('util');

とかに対して警告を出してきてちょっとイラっとする。どうもnode-jslintで使われているjslintがちょっと古いらしく、"util"がかつてnode.jsでpredefinedなglobalsだった、というのが残ってしまっているらしい。本家ではすでに修正されている模様。
Assume Node.js option falsely assumes global `util` · Issue #42 · douglascrockford/JSLint · GitHub
で、それを取り込んで更新してもらえれば良いのだけど 折角なのでnodelintに鞍替えしてみた。
https://github.com/tav/nodelint
こちらもnode-jslint同様にコマンドラインツールとしてjsファイルに対して実行することができるのだけど、細かい設定はconfigファイルのパスを渡す形になる。flymakeで使うにはちょっと残念…とは思ったもののとりあえず設定してみた。
設定は$HOME/local/etc/nodelint_config.jsにファイルを作り(実際には$HOME/.emacs.d/inits/etc/nodelint_config.jsにおいてそこにシンボリックリンクを貼ったけど それはgithub的な事情)

var options = {
    vars: true,
    error_prefix: "",
    error_suffix: ": "
};

と自分好みの設定を書く。flymakeでエラーパターンを解析するためにerror_prefixerror_suffixはシンプルにしておくのが良さげ。
で、flymake用の設定を書く。

(require 'flymake)
(add-to-list 'flymake-allowed-file-name-masks '("\\.js\\'" flymake-js-init))
(defun flymake-js-init ()
  (let* ((temp-file (flymake-init-create-temp-buffer-copy
                     'flymake-create-temp-inplace))
         (local-file (file-relative-name
                      temp-file
                      (file-name-directory buffer-file-name))))
    (list "nodelint" (list "--config" (concat (getenv "HOME") "/local/etc/nodelint_config.js") local-file))))
(defun flymake-js-load ()
  (interactive)
  (setq flymake-err-line-patterns
        (cons '("^\\(.+\\), line \\([[:digit:]]+\\), character \\([[:digit:]]+\\): \\(.+\\)$"
                1 2 3 4)
              flymake-err-line-patterns))
  (flymake-mode t))

ほとんどjslint用の設定と変わらず、コマンドラインオプションを調整したのと、警告出力パターンの変更にあわせてflymake-err-line-patternsのところを調整したくらい。

しばらくこれで使い続けてみます。

FileReaderを使って選択した画像の真ん中あたりを正方形に切り取って表示する

フォームで画像をアップロードする際にプレビュー表示したい、というとき、FileReaderが使えるブラウザだとそれを使って簡単に実現出来る。
html5の File API を使って、アップロード無しで画像プレビュー - 超自己満足プログラミング
「プレビュー表示する領域は正方形でサイズが決まっていて、縦長もしくは横長の画像がアップロードされたときは縦横比を変えずに真ん中あたりを正方形に切り取った形で表示したい」
というとき。

こんなイメージ。
無理矢理やってみた。

crop image from FileReader - jsdo.it - share JavaScript, HTML5 and CSS

実際に選択された画像ファイルのサイズが分からないので、一度見えない領域にimgタグで描画してしまい、その高さ/幅を取得し、すぐ消す。
その高さ、幅を元にプレビュー表示すべきサイズと縦横のオフセットを計算して、対象領域にセットする。
CSSを使ってサムネイル化する部分は下記記事を参考にさせていただきました。
CSSで画像のサムネイル化|エヌケー・テック株式会社【福島県郡山市】

var fr = new FileReader();
fr.onload = function() {
    var img = $('<img>').attr({
        src: fr.result
    }).load(function () {
        // get height & width
        var image = $(this);
        var height = image.height();
        var width  = image.width();
        image.parent().remove();
        // calculate size
        var css = { position: 'absolute' };
        if (height > width) {
            height *= 120 / width;
            width = 120;
            css.top  = - (height - 120) / 2;
            css.left = 0;
        } else {
            width *= 120 / height;
            height = 120;
            css.top  = 0;
            css.left = - (width - 120) / 2;
        }
        // add image
        $('#world').html(
            $('<img>').attr({
                src: fr.result,
                height: height,
                width: width
            }).css(css)
        );
    });
    $('body').append(
        $('<div>').css({
            height: '0px',
            width: '0px',
            overflow: 'hidden'
        }).append(img)
    );
};

canvasとか使えばもっと簡単にできるのかな(まだ触ったことなくて分かっていない)

orion editor updated.

http://livecoder.sugyan.com/で使っているeclipse OrionのJavaScriptエディタがupdateされていて名前空間などが変わってた。
Orion - Eclipsepedia
eclipse.orionだったものがorion.textview.TextViewなどに変更されている。
追従してlivecoderもアップデートしておいた。ついでに

  • Node v0.4.7 -> v0.4.8
  • Mongoose依存の解消
  • 編集時のコードを定期的に保存
  • configは結局ライブラリ使わずに設定JSをrequireする形で

あたりを反映。現在はllevalを使うバージョンを開発中。
facebookアカウントでのOAuthログインが上手くいかないので調査する。(6/5 fixed.)

Quine ruBy JavaScript版

Quine ruBy - まめめも
への挑戦。
Quine ruBy Perl版 - すぎゃーんメモ
に引き続き、JavaScript版。

     eval                                       ($a=
     'a=1;p=                                 this[1\
      &&"alert"                           ]?(a=0+0\
      )  ||alert:1     &&this["p"+     "rint"]?(  \
       1  &&print):console.log,u=unescape,q=u((  \
       0    ||"%27")),f=Function,x=f("t,n","v    \
        = [];while(n--)v+=t;return"+(s=u("%20") \
        )+"v"),n=u("%0a"),r=[45,95,145,194,243,1\
        +290,339,387,436,486,537,589,642,695,749\
       ,53 +750,858,913,969];for(i=19;i--;)$a= $a\
      [(e ="replace")](RegExp(".{"+r[i]+"}"),"" +(\
     "$&" )+(b=     u("%5c"))+n);if(a     ){c=[ 5,(\
    45), 5,55,    2, 31,58,43,2,31,(    58 ),53, 1,(\
   30),( 86),(       58),43,1,30,86,       58,53 ,5,(\
   45),5 ,28],o     =521;for(i=26;i--     ;){$a= $a[e\
  ](($a) .substr(0,o+c[i]%28),"$&"+u("%1b")+"["+ (d=[(\
  "0"),( "48;5;9"),"48;5;1","48;5;15"][Math["fl" +"oo"\
 +"r"](c[ i]/28)])+"m");  ;   o  +=c[i]%28+3+d. length}\
 };p(x(s   ,5)+"eval"+x(s   ,   39)+"($a="+n+x   (s,5)+\
q+$a+q+n    +"."+e+x(s,11)+"(/"+b+("x1b")+b+(    "[.*?"+\
"m/mig,"       +"/*")+x(s,11)+"qb*/"+q+q+(       "))"));'
.replace           (/\x1b\[.*?m/mig,/*           qb*/''))

https://gist.github.com/980193


ブラウザでは同じものを普通にalert。

Quine - jsdo.it - share JavaScript, HTML5 and CSS

Quine (write to <pre>) - jsdo.it - share JavaScript, HTML5 and CSS


コマンド実行の場合は目の色だけ変えるように。台詞つけるのは僕の腕では無理でした…

$ js --version
JavaScript-C 1.8.0 pre-release 1 2007-10-03
usage: js [-zKPswWxCij] [-t timeoutSeconds] [-c stackchunksize] [-o option] [-v version] [-f scriptfile] [-e script] [-S maxstacksize] [scriptfile] [scriptarg...]
$ wget jsdo.it/sugyan/quine/js -O qb.js 
$ cat qb.js
$ js qb.js
$ js qb.js | js


ruby, perlと比べてヒアドキュメント的なの書けないしで大変。結局末尾にバックスラッシュで文字列を繋げるという逃げ方をしてしまった。

node-jslintでflymake

javascript-modeでのflymakeに、今までSpiderMonkeyを使っていたけど、試しにnode-jslintを使ってみることにした。
GitHub - reid/node-jslint: The JavaScript Code Quality Tool — for Node.js.
node-jslintはnpmでinstallすると"jslint"コマンドを提供してくれるコマンドラインツール。lintnodeという、nodeでwebサーバを立ち上げてそこでjslintを実行する、というものもあるようだったが、そこまでするのはなぁ…ということで見送り。
まずはコマンドラインで使ってみる。

$ npm install jslint -g
$ cat hoge.js
a;
var b;
alert("hoge")
var c = {
    foo: "hoge",
    bar: "fuga",
};
$ jslint hoge.js

hoge.js
/*jslint node: true, es5: true */
  1 1,1: Expected an assignment or function call and instead saw an expression.
    a;
  2 3,14: Expected ';' and instead saw 'var'.
    alert("hoge")
$ jslint --no-es5 hoge.js

hoge.js
/*jslint es5: false, node: true */
  1 1,1: Expected an assignment or function call and instead saw an expression.
    a;
  2 3,14: Expected ';' and instead saw 'var'.
    alert("hoge")
  3 6,16: Unexpected ','.
    bar: "fuga",

デフォルトでes5オプションが有効になっていて、これだとObjectの最後のカンマの検出などがされない。"--no-es5"オプションを渡すと検出してくれる。
エラー出力の形式がjslint packageのreporterによって決められているので、これに従って"flymake-err-line-patterns"を調整。
EmacsWiki: Flymake Java Scriptを参考に。

(require 'flymake)
(add-to-list 'flymake-allowed-file-name-masks '("\\.js\\'" flymake-js-init))
(defun flymake-js-init ()
  (let* ((temp-file (flymake-init-create-temp-buffer-copy
                     'flymake-create-temp-inplace))
         (local-file (file-relative-name
                      temp-file
                      (file-name-directory buffer-file-name))))
    (list "jslint" (list "--no-es5" local-file))))
(defun flymake-js-load ()
  (interactive)
  (setq flymake-err-line-patterns
        (cons '("^ *[[:digit:]] \\([[:digit:]]+\\),\\([[:digit:]]+\\)\: \\(.+\\)$"
                nil 1 2 3)
              flymake-err-line-patterns))
  (flymake-mode t))

(add-hook 'js-mode-hook
          (lambda ()
            (flymake-js-load)))


下記設定をしておくことにより"M-e"でエラー行に飛んでminibufferにエラー内容が表示される。

(require 'flymake)
(global-set-key "\M-e" 'flymake-goto-next-error)
(global-set-key "\M-E" 'flymake-goto-prev-error)

;; gotoした際にエラーメッセージをminibufferに表示する
(defun display-error-message ()
  (message (get-char-property (point) 'help-echo)))
(defadvice flymake-goto-prev-error (after flymake-goto-prev-error-display-message)
  (display-error-message))
(defadvice flymake-goto-next-error (after flymake-goto-next-error-display-message)
  (display-error-message))
(ad-activate 'flymake-goto-prev-error 'flymake-goto-prev-error-display-message)
(ad-activate 'flymake-goto-next-error 'flymake-goto-next-error-display-message)

Cocoa Emacs特有? かどうかよく分からないけど、flymakeがsubprocessとしてjslintを実行する際にPATHとかexec-pathが通ってないとうまくいかないので下記設定を追加してある。

;; exec-pathにshellから得られる$PATHを追加
(loop for x in (reverse
                (split-string (substring (shell-command-to-string "echo $PATH") 0 -1) ":"))
      do (add-to-list 'exec-path x))
;; process-environmentも変更
(setenv "PATH" (substring (shell-command-to-string "echo $PATH") 0 -1))

手作りiPhoneTracker

これは不気味―iPhoneには過去の位置情報が逐一記録されていることが判明 | TechCrunch Japanという記事が話題に。
iPhoneで取得した位置情報が記録されている、というもの。そのデータを抜き出して可視化するツールが公開されている。
petewarden/iPhoneTracker @ GitHub
ソースが公開されているので覗いてみたところ、どうやら"$HOME/Library/Application Support/MobileSync/Backup"以下のファイルにそれらの情報を格納しているsqliteのファイルがあるらしく、そこからすべて抜き出しているらしい。ただBackupディレクトリ以下には無数のファイルがあり、どれがどれか分からない。それを判別するために"Manifest.mbdb", "Manifest.mbdx"というファイルを解析しているようだ。解析方法自体はiphone - How to parse the Manifest.mbdb file in an iOS 4.0 iTunes Backup - Stack Overflowにあるpythonのコードを参考にしているらしい。ここからsqliteのファイルが割り出せれば、比較的簡単にデータを抜き出して自由に扱うことができそう。
ということで作ってみた。Perlで。

#!/usr/bin/env perl
use strict;
use warnings;

use File::HomeDir;
use Path::Class 'dir';

my $home = File::HomeDir->my_home;
my @dir = sort {
    $b->stat->mtime <=> $a->stat->mtime
} dir("$home/Library/Application Support/MobileSync/Backup")->children;

my $mbdb = process_mbdb_file($dir[0]->file('Manifest.mbdb'));
my $mbdx = process_mbdx_file($dir[0]->file('Manifest.mbdx'));

my $dbfile;
for my $key (keys %{ $mbdb }) {
    $dbfile = $dir[0]->file($mbdx->{$mbdb->{$key}{start_offset}})->stringify;
}
die unless $dbfile;
print "dbfile: $dbfile\n";


sub process_mbdb_file {
    my ($mbdb) = @_;

    my $fh = $mbdb->openr;
    $fh->binmode;

    my $buffer;
    $fh->read($buffer, 4);
    die if $buffer ne 'mbdb';

    $fh->read($buffer, 2);
    my $offset = 6;

    my $data = +{};
    while ($offset < $mbdb->stat->size) {
        my $fileinfo = +{};
        $fileinfo->{start_offset} = $offset;
        $fileinfo->{domain}       = getstring($fh, \$offset);
        $fileinfo->{filename}     = getstring($fh, \$offset);
        $fileinfo->{linktarget}   = getstring($fh, \$offset);
        $fileinfo->{datahash}     = getstring($fh, \$offset);
        $fileinfo->{unknown1}     = getstring($fh, \$offset);
        $fileinfo->{mode}         = getint($fh, 2, \$offset);
        $fileinfo->{unknown2}     = getint($fh, 4, \$offset);
        $fileinfo->{unknown3}     = getint($fh, 4, \$offset);
        $fileinfo->{userid}       = getint($fh, 4, \$offset);
        $fileinfo->{groupid}      = getint($fh, 4, \$offset);
        $fileinfo->{mtime}        = getint($fh, 4, \$offset);
        $fileinfo->{atime}        = getint($fh, 4, \$offset);
        $fileinfo->{ctime}        = getint($fh, 4, \$offset);
        $fileinfo->{filelen}      = getint($fh, 8, \$offset);
        $fileinfo->{flag}         = getint($fh, 1, \$offset);
        $fileinfo->{numprops}     = getint($fh, 1, \$offset);
        $fileinfo->{properties}   = +{};
        for (1 .. $fileinfo->{numprops}) {
            my $key   = getstring($fh, \$offset);
            my $value = getstring($fh, \$offset);
            $fileinfo->{properties}{$key} = $value;
        }
        # 必要なのはこれが含まれているものだけ
        if ($fileinfo->{filename} eq 'Library/Caches/locationd/consolidated.db') {
            $data->{$fileinfo->{start_offset}} = $fileinfo;
        }
    };

    return $data;
}

sub process_mbdx_file {
    my ($mbdx) = @_;

    my $fh = $mbdx->openr;
    $fh->binmode;

    my $buffer;
    $fh->read($buffer, 4);
    die if $buffer ne 'mbdx';

    $fh->read($buffer, 2);
    my $offset = 6;

    my $filecount = getint($fh, 4, \$offset);
    my $data = +{};
    while ($offset < $mbdx->stat->size) {
        $fh->read($buffer, 20);
        $offset += 20;
        my $file_id = unpack("H*", $buffer);
        my $mbdb_offset = getint($fh, 4, \$offset);
        my $mode = getint($fh, 2, \$offset);
        $data->{$mbdb_offset + 6} = $file_id;
    }

    return $data;
}

sub getint {
    my ($fh, $size, $offset) = @_;

    $fh->read(my $buffer, $size);
    $$offset += $size;
    return oct('0x' . unpack("H*", $buffer));
}

sub getstring {
    my ($fh, $offset) = @_;

    my $buffer;

    $fh->read($buffer, 2);
    $$offset += 2;
    my $unpacked = unpack('H*', $buffer);
    return '' if $unpacked eq 'ffff';

    my $length = oct("0x${unpacked}");
    $fh->read($buffer, $length);
    $$offset += $length;
    return $buffer;
}

元のを普通に翻訳しただけ。これで対象のsqliteファイルを突き止めることができる。

$ perl dbfile.pl
dbfile: /Users/sugyan/Library/Application Support/MobileSync/Backup/****************************************/****************************************

ここからWifiLocation, CellLocationなどのテーブルの情報を読み取ることで位置情報の記録が辿れるようだ。
そういえばGoogle Fusion TablesっていうのがあってGoogleMapsのLayerとして使えるんだっけ、というのを思い出したので、取得したデータをそこに突っ込むスクリプトも書いてみた。

#!/usr/bin/env perl
use strict;
use warnings;

use Config::Pit;
use Data::Section::Simple;
use Furl;
use List::Util 'shuffle';
use Text::Xslate;

my $dbfile = shift or die;
my @data = ();
for my $table (qw/WifiLocation CellLocation/) {
    my $result = qx{ sqlite3 '$dbfile' 'SELECT Timestamp, Latitude, Longitude FROM $table;' };
    for my $row (split /\n/, $result) {
        my @col = split /\|/, $row;
        $col[0] += 31 * 365.25 * 24 * 60 * 60;
        push @data, \@col;
    }
}
@data = shuffle(@data);

my $token   = get_token();
my $tableid = create_table($token);

for (1 .. 20) {
    warn $_;
    my @queries = ();
    for (1 .. 500) {
        my $record = shift @data;
        push @queries, qq{INSERT INTO $tableid (timestamp, location) VALUES ($record->[0], '$record->[1],$record->[2]')};
    }
    my $query = join(';', @queries);
    insert_rows($token, $tableid, $query);
    warn 'insert ok';
    sleep 1;
}

my $tx = Text::Xslate->new(
    path => [
        Data::Section::Simple->new()->get_data_section(),
    ],
);
print $tx->render('tracker.tx', { tableid => $tableid });


sub get_token {
    my $conf = pit_get('google.com', require => {
        username => 'google user name',
        password => 'password',
    });

    my $furl = Furl->new;
    my $url  = 'https://www.google.com/accounts/ClientLogin';
    my $res  = $furl->post($url, [], [
        Email  => $conf->{username},
        Passwd => $conf->{password},
        accountType => 'GOOGLE',
        service     => 'fusiontables',
    ]);
    die unless $res->is_success;
    my ($token) = (split /\n/, $res->content)[2] =~ /^Auth=(.*)$/;

    return $token;
}

sub create_table {
    my ($token) = @_;

    my $furl = Furl->new;
    my $url  = 'https://www.google.com/fusiontables/api/query';
    my $res  = $furl->post($url, [
        Authorization  => "GoogleLogin auth=$token",
        'Content-Type' => 'application/x-www-form-urlencoded',
    ], [
        sql => q{CREATE TABLE tracker (location: Location, timestamp: DATETIME)},
    ]);
    die unless $res->is_success;
    my $tableid = (split /\n/, $res->content)[1];

    return $tableid;
}

sub insert_rows {
    my ($token, $tableid, $query) = @_;

    my $furl = Furl->new;
    my $url  = 'https://www.google.com/fusiontables/api/query';
    my $res  = $furl->post($url, [
        Authorization  => "GoogleLogin auth=$token",
        'Content-Type' => 'application/x-www-form-urlencoded',
    ], [
        sql => $query,
    ]);
    die unless $res->is_success;
}


__DATA__
@@ tracker.tx
<html>
  <head>
    <title>iPhone Tracker</title>
    <script type="text/javascript" src="https://www.google.com/jsapi"></script> 
    <script type="text/javascript">google.load("jquery", "1.5.2");</script> 
    <script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
    <script type="text/javascript">
function initialize() {
    var latlng = new google.maps.LatLng(36, 140);
    var options = {
        zoom: 6,
        center: latlng,
        mapTypeId: google.maps.MapTypeId.ROADMAP
    };
    var map = new google.maps.Map(
        document.getElementById("map_canvas"), options
    );
    var layer = new google.maps.FusionTablesLayer(<: $tableid :>, {
        map: map
    });
    google.maps.event.addListener(layer, 'click', function(e) {
        var date = new Date(e.row.timestamp.value * 1000);
        var div = $(e.infoWindowHtml);
        div.append('<br>').append(date.toLocaleString());
        e.infoWindowHtml = div.html();
    });
}
    </script>
  </head>
  <body onload="initialize();">
    <div id="map_canvas" style="width:100%; height:100%"></div>
  </body>
</html>

残念なことにPerlでFusionTablesAPIを扱うライブラリは無いようだったけど、INSERTするだけだし普通にリクエストを生成して送る。データサイズの制限などもあるようなのでシャッフルして10000件程度だけINSERTするようにした。
完了後、生成したテーブルidをFusionTablesLayerとして扱うmapsのHTMLを出力する。

$ perl fusion.pl '/Users/sugyan/Library/Application Support/MobileSync/Backup/****************************************/****************************************' > result.html
$ open result.html

おそらくVisibilityがPrivateだとデータが参照出来ないので、 http://www.google.com/fusiontables/Home から上記で生成したテーブルを選択し、"Unlisted"に設定する(他人から普通にアクセスできるようになってしまうので扱いには注意)。

昨年夏に九州に行ったのとか、秋に青森に行ったのとか、先月仙台に帰ったのとか、すべて記録されているのが確認出来た。


https://gist.github.com/934815

追記

sqlite3のデータを読むならDBIモジュールを使いましょう!!
多くの方からご指摘うけました。ありがとうございました。