縦横縮尺率の変更、ドラッグによるスクロール・ホイールによる拡大縮小機能追加、画面上にないオブジェクトをcanvasに乗せないことで動作を軽量化、canvasのサイズ選択追加、京王線系統のDB追加

六帖webアプリ
kickzone/TrainNavi2 · GitHub
かなり進んだ。スクロールと拡大縮小、さらにオブジェクトの省略まで同時に実装できるとは思わなかった。次は、パッケージファイルの読み書きが一番大きな変更になりそうだ。
既知の不具合:スクロールすると線路が途切れることがある

1点ハマったこととして、firefoxはマウスホイールイベントが特殊で、mousewheelではなくDOMMouseScrollとかいうイベントを使わないといけないらしいです。クロスプラットフォームなコードは次の通り。

function OnCanvasMouseWheel(e){
var toScale = scale;
e.preventDefault();
var delta = e.originalEvent.deltaY ? -(e.originalEvent.deltaY) : e.originalEvent.wheelDelta ? e.originalEvent.wheelDelta : -(e.originalEvent.detail);
if (delta > 0){
toScale /= 2;
} else {
toScale *= 2;
}
SetScale(toScale);
return false;
}
canvas.addEventListener("mousewheel" , OnCanvasMouseWheel, false);
var mousewheelevent = 'onwheel' in document ? 'wheel' : 'onmousewheel' in document ? 'mousewheel' : 'DOMMouseScroll';
$("canvas").on(mousewheelevent, OnCanvasMouseWheel);

緯度と経度の算出方法再考

Google MAPS APIで円を描きたい
によれば、経度1度と緯度1度あたりの距離は、異なる(当たり前か)。現在、x-y軸方向に同一倍率で路線図を描いている。つまり、xyどちらかが圧縮された路線図になってしまっている。七面倒臭いけどxy両方の倍率を用意して、それぞれ別個に単位当たりの長さを算出、xy別々に掛け算をしないといけない、ということか。
これも当然なんだろうけど、北緯・東経が変われば、単位当たりの長さも変わる。これは参った。単位当たりの長さが一定ならば、北緯・東経さえわかってれば、緯度・経度の差に倍率掛けるだけである基準点に対する絶対位置を一意に決められる。でも単位当たりの長さが変わるのであれば、

経度1度あたりの長さ(m) = (π * ER * (1 – e2)) / (180 * (1 – e2 sin2(θ))3/2)
緯度1度あたりの長さ(m) = (π * ER * cos(θ)) / (180 * (1 – e2 sin2(θ))1/2)

こんなの積分できないっす。。
θはほとんど変動しないと考えて、2点の単位当たりの長さの平均で近似しようかねぇ。まずは誤差がどのくらいになるかの調査をしなければいかん。


平日・休日ダイヤに対応、行先表示の省略に対応、DBの整備

六帖webアプリ
kickzone/TrainNavi2 · GitHub
DBを整備したら千代田線の行先表示の「代々木上原」の文字がでかくてそれがずっと動き回って怖いので、「代」の1文字だけ表示するモードを作成した。
次は拡大縮小とスクロール。仕事と並行して作業していたので2倍の労力が必要になった。もう疲れたので、平日はしばらく大きな更新はやめる。集中力もガタ落ち、勉強も身が入らずよくない。日曜日まで待とう。


行先表示

六帖webアプリ
kickzone/TrainNavi2 · GitHub
祝日なのでゆっくり仕事しながらちょこちょこと行先表示を実装、と思ったら、夜に仕事がどっと来て涙目。行先表示まで実装はできた。これで、ある程度見た目がよくなった。
列車の行先名を普通の順序で並べる(相模大野)か、逆順で並べる(野大模相)かの境目を、線路が右下45度の直線より下になるかどうか、にした。
野大模相\相模大野
文字は逆に、右上45度の直線より下になるかどうかが分かれ目。実装をけちって、駅停車時の状態の2駅間の表示を基準に、線路上はその中間になるようにしたら、藤沢や片瀬江ノ島あたりの線路がちょうどそのくらいの角度だったので、意図せず駅名が反転アニメーションするようになった。これはこれで面白いかも。
ところどころ連絡する列車の参照が取れてないのかDBが間違ってるのか、行先表示が間違っているところがある。次回はこのデバッグからか。


デバッグと今後の予定

六帖webアプリ
kickzone/TrainNavi2 · GitHub
用事を済ませてからデバッグ。一応、動作するようになった。今回のデバッグで、よーく覚えておかなければいけないことは、
・EaselJSでテキストオブジェクトを扱う際は、オブジェクト自体の大きさに気を付けること。点と点を結んだ直線と比べると、同じ座標にオブジェクトを置いても、getMeasuredWidth()/2, getMeasuredHeight()/2の分だけ、右下にずれる。これは、四角形などのShapeにも言えること。
・さらに、右下、ということに気が付いてなかった。そう、パソコンのXY軸は、ふつーの数学で使うXY座標軸とは違う。Y軸が180度反転している。ここでハマった。左90度回転のベクトルを作ろうと思ったら、通常の座標系でいう、右90度回転と同じ計算をしなければいけない。ベクトルの計算例で作成したコードを載せておきます。

//ベクトルの長さを計算
function norm(p){
return Math.sqrt(Math.pow(p.x, 2) + Math.pow(p.y, 2));
}
//p1p2, p3p2の角の二等分線を単位ベクトルで得る
function calcBisectUnitVector(p1, p2, p3)
{
//単位ベクトル同士を足すと出来上がり
var p1p2 = {x: p2.x - p1.x, y: p2.y - p1.y};
var nrmp1p2 = norm(p1p2);
var p3p2 = {x: p2.x - p3.x, y: p2.y - p3.y};
var nrmp3p2 = norm(p3p2);
var bisect = {x: p1p2.x/nrmp1p2 + p3p2.x/nrmp3p2, y: p1p2.y/nrmp1p2 + p3p2.y/nrmp3p2};
if(bisect.x == 0 && bisect.y == 0){
//1直線上に3点があった場合、打ち消しあって0になってしまう
//このときはp1p2の法線をリターン
return calcNormalUnitVector(p1, p2);
}
var nrmBisect = norm(bisect);
bisect.x /= nrmBisect;
bisect.y /= nrmBisect;
//p1p2の左側に二等分線が来るようにしたい
//p1p2と外積をとって、正なら反転させる
var cp = p1p2.x * bisect.y - p1p2.y * bisect.x;
if(cp > 0){
bisect.x = -bisect.x;
bisect.y = -bisect.y;
}
return bisect;
}
//p1p2の法線単位ベクトルを得る
//左90度回転
function calcNormalUnitVector(p1, p2)
{
//(x, y)の左90度回転ベクトルは(-y, x)
//しかしながら、PCの座標系はyが反転しているので、(y, -x)にしないといけない
var normalVec = {x: p2.y - p1.y, y: p1.x - p2.x};
var nrmNormal = norm(normalVec);
normalVec.x /= nrmNormal;
normalVec.y /= nrmNormal;
return normalVec;
}

前回のTrainNaviと比べると、JSONをやめて、サーバーとの通信の冗長性をひたすらなくしたことで、ローカルでさえ10秒ほどかかっていたローディングが、無料サーバー経由でもほんの1秒ほどで済むようになった。自分でもびっくり。一方、描画はのろい。行先表示もまだ未実装だというのに、15fpsでもCPU使用率が20%台をキープしている。非常に簡単なオブジェクトしか描画してないんだから、もっと軽くできるはずだ。例えば列車はテキストじゃなくてShapeにした方がいいかも。キャッシュ機能も使うべきかもしれない。

今後の予定:(優先順)

  • 行先表示
  • 拡大縮小、スクロール
  • DB拡充
  • 駅名表示(どこに表示するかが問題)
  • パッケージファイルの読み込み
  • 動作速度の改善

追加したい機能(優先順)

  • スクロールに合わせた画面表示をする。画面にないオブジェクトを削除して動作を軽くする。スクロールして画面内に入ってきたところではじめて表示する。
  • 通過点のエディット機能、ベジェ曲線で完璧な線路を引くためのGUI作成。ベジェ曲線 – diary 六帖
  • GoogleMapと連携する。地図上を列車が走る。
  • スキン機能。あらゆる路線図を作れるようにする。
  • ターゲットとなる列車を決めて、強調表示する。今どのあたりを走っているかが分かる。
  • 駅をクリックしたら情報を表示する。あと何分で電車が来るとか、目的地を決めたらそこまでかかる時間とか。
  • 列車をクリックしたら情報を表示する。現在速度とか表定速度とか、次はどこに止まるかとか。
  • 運賃を表示できるようにする。FareMap(自作)の機能を全部移植する。
  • 時刻表を自動生成する。
  • この際、NAVITIMEみたいな最短経路・最安経路生成機能も付けてしまう。

列車が動いた

六帖webアプリ
kickzone/TrainNavi2 · GitHub
非常に不審な動き。まだまだバグだらけのようだ。
まず全ての列車が線路の東側しか走ってない。さらに、ベクトルの計算式がおかしいのか、駅によって場所が激しくずれる。新宿近辺は特にカクカクしている。列車の読み込み機能にもバグがあるようで、しばらくすると列車がいなくなる。バグつぶしは仕事だけでたくさんだ。半日以上かかってもあまり進まなかったなぁ。つかれた。
さて、Ajaxで非同期処理をする際、処理の終了を待って先に進みたいこともある。このような場合はsetIntervalを使って、次のように書けばよいことが分かった。

DB.readTrains(startTime, dbReadTime, trains, AddTrains); //Ajax処理 終わったらonInitのフラグがなくなる
var wait = setInterval(function() {
if (!onInit) {
//初期化が終わったのでIntervalを削除。
clearInterval(wait);
//イベント登録、開始
createjs.Ticker.addEventListener("tick", OnTickMain);
createjs.Ticker.setFPS(fps);
}
}, 100); //100msごとにフラグを確認

JavaScript→PHPに多次元配列を渡す

PHPにデータを渡すために、POSTを使うが、POSTは一次元配列しか渡せない。この制限を回避するために、いろいろな方法があるようだ。
参考リンク:
jQueryのAjaxで多次元配列をPOST – ゆっくり*ゆっくり
JSで作った多次元配列をPOSTでPHPに渡す方法 | MiLKySHADe * ミルキーシェイド
ハッシュを使わないなら、JSONに一度変換して、PHP側でデコードするのが一番楽そうだと分かった。
JavaScript側

var ary = [
[92, 88, 64, 86],
[78, 92, 96, 81],
[68, 56, 84, 70]
];
var aryJSON = JSON.stringify(ary);
$.ajax({
async: true,
type: "POST",
url: "ajax.php",
data: { Ary : aryJSON }
}).done(function( msg ) {
//なんか処理
});

PHP側

$ary = json_decode($_POST['Ary']);
for($i=0; $i<count($ary); $i++)
{
	for($j=0; $j<count($ary[i]); $j++)
	{
		//$ary[i][j]を利用してなんか処理
	}
}

こんな風にできた。オブジェクトを渡したり、連想配列を渡したりした場合は、まだどうなるか不明。


列車の始点と終点

列車オブジェクトを画面上に配置するにあたって、その座標が問題となる。路線の上にそのまんま乗せると、上り電車と下り電車が衝突する。怖いし見づらい。したがって、若干線路からずらす必要がある。
日本の電車は左側通行なので、上図でいうと直線ABから一定距離離れた直線A1B’上を列車が動けばよい。この一定距離をrとする。
まず点A1の位置を求める。駅Aの位置を(Xa, Ya), 駅Bの位置を(Xb, Yb)とする。ベクトルABは(Xb-Xa, Yb-Ya)となる。
左側通行を実現するために単位法線ベクトルN=(Nx, Ny)を求める。ABを左側に90度回転して、さらにABの長さで割る。ABを90度回転したベクトルは(Ya-Yb, Xb-Xa)なので
Nx = (Ya-Yb) * Math.sqrt((Xb-Xa)^2 + (Yb-Ya)^2)
Ny = (Xb-Xa) * Math.sqrt((Xb-Xa)^2 + (Yb-Ya)^2)
あとはこれをr倍して(Xa, Ya)に足してやれば、点A1の位置が求まる。これが列車の始点になる。rは見た目から考えて一定値にはにならないだろう。倍率やスキンに応じて変更する。
次に駅Bの停車位置B1を求める。B1は駅Bに法線ベクトルを足したB’の位置ではない。B’とB”の間をつなぐ経路が必要だ。これを実現するため、駅Cに登場してもらう必要がある。B, CをBCから一定距離離した点B”, C’を考える。すると直線A1B’と直線B”Cの交点が、求める列車の停止位置B1になる。
さてB’、B”、C’はA1と同じ手法を使って求められるので、あとは、2直線の交点を求める作業だけだ。二元連立方程式を使ってもいいけど、でもそれだと万が一X軸Y軸に垂直な直線になった時困るなぁ。と思って探していたら、ちゃんと交点を求める手法があった。外積を使うらしい。
4点からなる交点の求め方 画像処理ソリューション
この式を使うなら、それぞれA1=(X1, Y1), B’=(X2, Y2), B”=(X3, Y3), C’=(X4, Y4)とすれば
S1 = {(X4 – X3) * (Y1 – Y3) – (Y4 – Y3) * (X1 – X3)} / 2
S2 = {(X4 – X3) * (Y3 – Y2) – (Y4 – Y3) * (X3 – X2)} / 2
交点の座標は
X = X1 + (X2 – X1) * S1 / (S1 + S2)
Y = Y1 + (Y2 – Y1) * S1 / (S1 + S2)
なんと4行で書ける!エレガントすぎる!図形的には、外積で三角形の面積S1とS2を求め、その比を使ってベクトルを定数倍する、という意味になる。これは素晴らしい。
あとは、順次C1,D1と列車の位置を求めていくことになる。他線への乗り入れがあることを考えると、列車の位置は駅によって一定ではない。列車ごとにそれぞれ計算することになるだろう。また、行き先表示の文字列は、上りは左側・下りは右側、なんて単純にはならないから、さっき求めた単位法線ベクトルを使って、線路と垂直な位置に文字を配置するとよいのでは。


路線の表示

六帖webアプリ
kickzone/TrainNavi2 · GitHub
駅、線路の描画のみ完成。現時点では、路線を選択してStartボタンを押すと駅と路線が描画されるだけ。将来の拡張を見越して、スクロール前の絶対位置や倍率なども考慮して設計しながらコードを書くので骨が折れる。でも後で楽になるはず。
ソースを分散したので、開発効率は大幅に上がった。以前のようにメモ帳だけ、しかも単一ファイルで開発していてはデバッグが大変すぎて苦しかった。来週は電車を走らせてみよう。

課題
Javascriptはクラスベースの言語ではない。仕事でC++やC#ばかり使っているので、どうしてもオブジェクトの共通メソッドとしてのクラスメソッドを実装したくなってしまう。しかしながら、クラスメソッドとしてプロトタイプにメソッドを記述すると、thisの嵐になってしまう。

TNStation.prototype = {
makeObject : function(cj, stage, coefXY, latView, lonView){
this.stage = stage;
this.cj = cj;
var sha = new cj.Shape()
this.shape = sha;
var gr = sha.graphics;
//描画
gr.beginStroke(this.line.lineColor).beginFill(this.line.lineColor).drawRect(-5, -5, 10, 10, 1);
//stageに追加
stage.addChild(sha);
//初期位置、係数を保存しておく
this.initLatView = latView;
this.initLonView = lonView;
this.coefXY = coefXY;
},
setScale : function(scale){
this.scale = scale;
this.shape.scaleX = scale;
this.shape.scaleY = scale;
},
moveObject : function(latView, lonView, centerX, centerY){
var x = ((this.longitude - this.initLonView) * this.coefXY + (lonView - this.initLonView) * this.coefXY) * this.scale + centerX;
var y = ((this.initLatView - this.latitude) * this.coefXY + (this.initLatView - latView) * this.coefXY) * this.scale + centerY;
this.shape.x = x;
this.shape.y = y;
}
}

thisばっかりで読みにくいし、thisを付け忘れやすくエラーの原因にもなる。なんとかならないか。