Expressでログを出力する

おぼえ書き.

http://d.hatena.ne.jp/higed/20130907/1378558020
でWebサーバを作った時にExpressを使ったが,
デバッグログの出し方が分からなかったので困った,

var express = require("express"),app = express();
app.use(express.logger("dev"));

loggerミドルウェアで[dev]フォーマット使えばレスポンスタイムが表示されるんですね.
便利.

canvas+WebSocket(node.js)で画像にポインタを表示する[Firefox限定]

canvas上でマウスクリックしたポインタと背景画像を画像合成して,
WebSocketで送信するサンプルアプリを作成しました.
とりあえずFirefox限定です.

Chromeだと合成した画像が表示されません.
StackOverFlowでimg.onload = function() {}でイケると書いてありましたが,
動きませんでした.

動作

  • クライアント画面のcanvas上でマウスクリックをするとjpegの背景画像と,ポインタを表示する
  • マウスクリックをトリガに背景画像とポインタを合成して,WebSocketサーバに送信する
  • WebSocketサーバは受信した画像を全クライアントへブロードキャストする
  • 全クライアントからポインタ情報を送信可能

構成

  • WebSocketサーバ:node.js + ws
  • HTTPサーバ:node.js + express

WebSocketサーバ(localhost:8080) <-> Clients(Firefox) <-> HTTPサーバ(localhost:8080)

実行手順

  • コードを以下の通り配置する
/hoge
 SampleWebSocketServer.js
 SimpleWebSocketServer.js
/hoge/public
 SampleWebSocketCanvasClient.html
 backgtround.jpg
  • 2つのコンソールで以下を実行する
node SimpleWebSocket.js
node SimpleWebServer.js

Code

SimpleWebServer.js(HTTPサーバ)
var express = require("express"),
	app = express();

app.configure(function() {
	app.use(express.static(__dirname + "/public"));
});

app.use(express.logger("dev"));

app.listen(8000);
SampleWebSocketServer.js(WebSocketサーバ)
var WebSocketServer = require("ws").Server;
var clients = [];

function initialize() {

	wss = new WebSocketServer({port:8080});
	wss.on("connection", function(ws) {

		clients.push(ws);
		ws.on("message", function(data, flags) {
			var type;

			// Check binary
			if(flags.binary) {
				console.log("binary");
				type = "binary";
				broadcast(data, flags.binary);
			} else {
				console.log("text");
				type = "text";
				broadcast(data. flags.binary);
			}
		});

		// Broadcast
		function broadcast(data, flag) {
			clients.forEach(function (client, i) {
				client.send(data, {binary:flag, mask:false});
			});
		}

		ws.on("close", function() {
			console.log("disconnected");
			clients = clients.filter(function (client) {
				return (client === ws) ? false : true;
			});
		});
});
}

initialize();
SampleWebSocketCanvasClient.html(クライアント)
<!DOCTYPE HTML>
<html lang="ja-JP">
<head>
	<meta charset="UTF-8">
	<script type="text/javascript">

	// constant
	var canvasFrameEndX = 300;
	var canvasFrameEndY = 300;

	// canvas
	var canvas;
	var context;
	var saveImage;

	// global variables
	var img;
	var host = "ws://localhost:8080";
	var ws = new WebSocket(host);

	function initialize() {

		// DOM
		img = document.getElementById("img");
		canvas = document.getElementById("canvas");
		// Canvas context
		context  = canvas.getContext("2d");

		// 画像の読み込み
		canvas.addEventListener('mousedown', function(e) {
			clear();
			var x;
			var y;

			if(e.hasOwnProperty("offsetX")) {
				x = e.offsetX;
				y = e.offsetX;
			} else {
				// for Firefox
				x = e.layerX;
				y = e.layerY;
			}
			draw(x, y);
			
		}, false);

		// Canvas
		context.strokeStyle = '#FF0000';
		context.lineWidth = 5;
		context.strokeStyle ="";

		// WebSocket
		ws.onopen = function() {
			console.log("WebSocket connect");
			ws.onmessage = function(e) {
				if(e.data instanceof Blob) {
					console.log("blob");
					img.src =
						(URL || WebKit).createObjectURL(e.data);
				} else if(typeof e.data === "string") {
					console.log("string");
					console.log(e.data);
				}
			};
		};

	}
	function clear() {
		context.clearRect(0, 0, canvasFrameEndX, canvasFrameEndY);
	}
	function draw(x, y) {
		var background = new Image();
		background.src = "background.jpg";
		background.onload = function() {
			// img.onloadに入れても動かない
		};
		context.drawImage(background, 0, 0);		
		context.strokeRect(x, y, 50, 50);

		/***
		  canvas に絵を書くコード
		***/
		var type = 'image/png';
		// canvas から DataURL で画像を出力
		var dataurl = canvas.toDataURL(type);
		// DataURL のデータ部分を抜き出し、Base64からバイナリに変換
		var bin = atob(dataurl.split(',')[1]);
		// 空の Uint8Array ビューを作る
		var buffer = new Uint8Array(bin.length);
		// Uint8Array ビューに 1 バイトずつ値を埋める
		for (var i = 0; i < bin.length; i++) {
		  buffer[i] = bin.charCodeAt(i);
		}
		// Uint8Array ビューのバッファーを抜き出し、それを元に Blob を作る
		var blob = new Blob([buffer.buffer], {type: type});
		//console.log(blob);
		ws.send(blob);
	}

	window.addEventListener("load", initialize, false);
	</script>
	<title>canvas sample</title>
	<style type="text/css">
	#canvasdiv {
		width: 300px;
		border-style: solid;
		border-color: green;
	}
	#canvas {
		width: 300px;
		border-style: solid;
		border-color: red;
	}
	#recieveimage {
		width: 300px;
		border-style: solid;
		border-color: blue;
	}

	</style>
</head>
<body>
<div id="canvasdiv">
	<canvas id="canvas" width="300px" height="300px"></canvas>
</div>
overlay
<div id="recieveimage">
	<img src="data:image/jpeg,base64" id="img" width="300px"/>
</div>
</body>
</html>

言語仕様の参考サイト

HTML5勉強の前に,JavaScriptの勉強中.
サイ本で勉強するつもりでしたが,
時間がかかりそうなため,
サンプルを作りながら必要に応じてサイ本を見る勉強方法に変更しました・.

JavaScriptはWebブラウザ毎にAPIが異なったりするためか,
まとまって解説するページがありませんでした.

探した中でMDN(Mozilla Developer Network)が分かりやすかったです.
https://developer.mozilla.org/ja/docs/Web/JavaScript

サンプルをさっさと書こう.

ListViewに追加,削除するサンプル

ListViewの勉強で作ったサンプル.

参考にしたページは以下.
http://techbooster.org/android/ui/9039/
http://www.mitoroid.com/category/android/android_listview2.php

動作は以下の通り.

  • 動作環境はAndroid 3.0以降(FragmentManagerを使っているので)
  • テキストボックス入力してボタンで追加
  • ListView内のアイテムを長押しで削除

意外と追加,削除といった動的な処理のサンプルは少ないようです.
探すとありますね.
DB操作も一緒になったサンプルとか.
http://handin.sakura.ne.jp/archives/368

package com.example.samplelistview;

import android.os.Bundle;
import android.app.Activity;
import android.app.AlertDialog.Builder;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.FragmentManager;
import android.content.DialogInterface;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.Toast;

/*
 * ListViewのアイテムを追加,削除するクラス
 * テキストボックスでアイテムを入力する.
 * ListViewアイテムの長押しで当該アイテムを削除する.
 */
public class MainActivity extends Activity {

    private static final String DEBUG = "DEBUG";
    private ArrayAdapter<String> adapter = null;
    private Button _button = null;
    private ListView _listView = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // LayoutファイルのListViewのリソースID
        _listView = (ListView) findViewById(R.id.list_view);

        // Androidフレームワーク標準のレイアウト
        adapter = new ArrayAdapter<String>(getApplicationContext(),
                android.R.layout.simple_list_item_1);

        // ListViewの初期表示
        adapter.add("Japan");
        adapter.add("Tokyo");

        _listView.setAdapter(adapter);

        // ListViewアイテムを選択した場合の動作
        _listView.setOnItemClickListener(new OnItemClickListener() {

            @Override
            public void onItemClick(AdapterView<?> parent, View view,
                    int position, long id) {
                // 選択したListViewアイテムを表示する
                ListView list = (ListView) parent;
                String selectedItem = (String) list.getItemAtPosition(position);
                Toast.makeText(getApplicationContext(), selectedItem,
                        Toast.LENGTH_LONG).show();
                Log.d(DEBUG, selectedItem);
            }
        });

        // ListViewアイテムの長押しでListViewアイテムを削除する
        // リスナーはAdapaterView.onItemLongClickListener()を利用する
        // 利用しないとListViewのアイテムを取得できない
        _listView.setOnItemLongClickListener
                (new AdapterView.OnItemLongClickListener() {

                    @Override
                    public boolean onItemLongClick(AdapterView<?> parent,
                            View view, int position, long id) {
                        ListView list = (ListView) parent;
                        String selectedItem = (String) list
                                .getItemAtPosition(position);
                        Log.d(DEBUG, "Long click : " + selectedItem);

                        showDialogFragment(selectedItem);
                        return false;
                    }
                });

        // EditTextのエントリをListViewアイテムに追加する
        _button = (Button) findViewById(R.id.add_list_button);
        _button.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {

                EditText editText = (EditText) findViewById(R.id.add_edit_text);
                String entry = editText.getText().toString();

                if (entry.equals("")) {
                    Log.d(DEBUG, "Entry is empty");
                } else {
                    addListData(entry);
                }
                // ボタン押下後のエントリ文字列を削除する
                editText.setText("");
            }
        });
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    // ListViewアイテムにエントリを追加するメソッド
    private void addListData(String entry) {
        adapter.add(entry);
    }

    // FragmentManagerでDialogを管理するクラス
    private void showDialogFragment(String selectedItem) {
        FragmentManager manager = getFragmentManager();
        DeleteDialog dialog = new DeleteDialog();
        dialog.setSelectedItem(selectedItem);
        
        dialog.show(manager, "dialog");
    }
    
    /*
     * 削除ダイアログを生成する内部クラス
     * 内部クラスは外部クラスのインスタンスを直接参照できないため,
     * Activity#getActivity()で外部クラスのインスタンスを取得している.
     */
    public static class DeleteDialog extends DialogFragment {
        
        private static final String DEBUG = "DEBUG";
        /* 選択したListViewアイテム */
        private String selectedItem = null;

        // 削除ダイアログの作成.
        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            Log.d(DEBUG, "onCreateDialog()");
            
            Builder builder = new AlertDialog.Builder(getActivity());
            builder.setTitle("Delete entry.");
            builder.setMessage("Are you really?");
            
            // positiveを選択した場合の処理.
            // リスナーはDialogINterface#onClickListener()
            // を使うことに注意.
            builder.setPositiveButton("Yes I'm serious.",
                    new DialogInterface.OnClickListener() {
                
                // 外部クラスのインスタンスを直接参照することができないため,
                // Activity#getActivity()でActivityのインスタンスを取得する
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    MainActivity activity = (MainActivity) getActivity();
                    activity.removeItem(selectedItem);
                }
            });
            AlertDialog dialog = builder.create();
            return dialog;
        }
        
        // 選択したアイテムをセットする.
        // HACK:削除ダイアログ自身に選択したアイテムを渡せないため,
        // ダイアログをユーザが呼び出した際に,Activityで選択した項目を保持しておく.
        public void setSelectedItem(String selectedItem) {
            Log.d(DEBUG, "setSelectedItem() - item : " + selectedItem);
            this.selectedItem = selectedItem;
        }
    }

    // 選択したアイテムを削除する.
    protected void removeItem(String selectedItem) {
        Log.d(DEBUG, "doPositiveClick() - item : " + selectedItem); 
        adapter.remove(selectedItem);
    }
}

Nexus7のUSBドライバインストール

Google Nexus7にUSBドライバをインストール.
そのまま挿せば使えるかと思っていたが,別途インストール手順が必要だった.
2.3.3のサンプルを入れてみたけど,上手く動かないのね...

AlertDialogを使ってみる.

物凄い間が空いた再開です.

AndroidでAlertDialogを使って,Googleのトップページを表示します.
タブ幅を3にすると,枠の端で折り返されてしまうことが多いのだろうか.

プログラム用に上手く表示できるようにしたい.
SyntaxHighLighterが上手く動かない...

  • activity_mock_alert_dialog.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MockAlertDialogActivity" >

    <Button
        android:id="@+id/dialog_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:text="Dialog" />


</RelativeLayout>
  • MockAlertDialogActivity.java
package com.example.mockalertdialog;

import android.net.Uri;
import android.os.Bundle;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;

/**
 * http://google.co.jpを表示するアクティビティ ボタンを押すとダイアログ表示->OKとすると,暗黙的Intentでブラウザに遷移する.
 * 
 */
public class MockAlertDialogActivity extends Activity {

    private Button dialogButton = null;
    private AlertDialog.Builder alertBuilder = null;
    private String URLString = null;
    private Intent intent = null;

    // State
    @Override
    protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_mock_alert_dialog);
    }

    @Override
    protected void onResume() {
	super.onResume();

	// URL
	URLString = "http://google.co.jp";

	// AlertDialogの生成
	alertBuilder = new AlertDialog.Builder(MockAlertDialogActivity.this);

	dialogButton = (Button) findViewById(R.id.dialog_button);

	// ボタンのリスナーを無名クラスで生成
	dialogButton.setOnClickListener(new OnClickListener() {

	    // クリック時のメソッド
	    @Override
	    public void onClick(View v) {
		// [Title]はタイトル部, [Message]は本文に該当する
		alertBuilder.setTitle("Warning!");
		alertBuilder.setMessage(URLString);

		// Positive, Neutral, NegativeをAlertDialogでは選択可で,使用方法は自由.
		// Positiveの選択をした場合
		alertBuilder.setPositiveButton("OK",
			new DialogInterface.OnClickListener() {

			    @Override
			    public void onClick(DialogInterface dialog,
				    int which) {

				// 暗黙的インテントの生成.URLの文字列をパースする.
				// Intentを0以上とするとActivity#onActivityResult()に戻って,アプリケーション/Activity間で結果を受け取る事ができる.
				intent = new Intent(Intent.ACTION_VIEW, Uri
					.parse(URLString));
				startActivityForResult(intent, -1);
			    }
			});

		// Neutralの選択をした場合
		alertBuilder.setNeutralButton("Neutral",
			new DialogInterface.OnClickListener() {

			    @Override
			    public void onClick(DialogInterface dialog,
				    int which) {
				Toast.makeText(getApplicationContext(),
					"Neutral", Toast.LENGTH_SHORT).show();
			    }
			});

		// Negativeの選択をした場合
		alertBuilder.setNegativeButton("Cancel",
			new DialogInterface.OnClickListener() {

			    @Override
			    public void onClick(DialogInterface dialog,
				    int which) {
				dialog.cancel();
			    }
			});

		// AlertDialogの表示
		alertBuilder.show();
	    }
	});
    }

    @Override
    protected void onPause() {
	super.onPause();
    }

    // Menu
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
	// Inflate the menu; this adds items to the action bar if it is present.
	getMenuInflater().inflate(R.menu.activity_mock_alert_dialog, menu);
	return true;
    }
}

SVNでnon-lFのエラー

以前に記載したnon-LFのエラーですが,対処したと思った下記エラーが,

Cannot accept non-LF line endings in 'svn:log' property 

コミット時に再度エラーが出ました.
ReadyNAS上のリポジトリにコミットできない - higedの日記

grayhole: subversion Cannot accept non-LF アホかい
ここらへんのリンクから行けますが,

subversion: Discussion topic

1. It is possible unknowingly commit a value for svn:ignore which
   mixes eol-style, using a version of Subclipse on Windows.

2. The presence of this property value effectively breaks svnsnync
   1.6.0 because it insists on eol-style purity.

文字コードのチェックで止まるみたいですね.
今回はソースの文字コードも変更したのですが,
コミット時のログも同様に文字コードのチェック対象となるようです.

そこで,暫定対処としてはログをコミット後に追記します.
手順は下記の通り.

  1. コミット時にログを空白のままコミット
    1. 作業ディレクトリを選択して[右クリック]->[TortoiseSVN]->[ログを表示(L)]
    2. 最新のリビジョンを選択して[右クリック]->[ログメッセージを編集]
    3. 「ログメッセージを編集」画面で,ログを投入して,[OK]を押下
    4. 「ログメッセージ」画面で[OK]を押下

文字コードのチェック機能を入れる必要は特にないでしょ...