2012年6月29日金曜日

PhoneGapで方位(方角)を取得する

さきほどはAndroidのネイティブでセンサー使って方位を取得しました。
http://teru2-bo2.blogspot.jp/2012/06/android.html

試しにとおもってPhoneGapでも作ってみました。
せっかくPhoneGapなんでiOS版とAndroid版と作ろうと思ったんですがまったく同じアプリをつくるのに別々で作らないといけないってのがいやで前に紹介したMonacaを使ってみました。
Monaca beta

コードは以下
<!DOCTYPE HTML>
<html>
    <head>
        <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
        <title>Plain Project Skeleton</title>
        <script type="text/javascript" src="plugins/plugin-loader.js"></script>
        <script type="text/javascript">
            // Set virtual screen width size to 640 pixels (横幅640pxに設定)
            monaca.viewport({width: 640});

            var watchID;
            var orient_range = new Array(
                    45,
                    135,
                    225,
                    315
                );
            var orient_string = new Array(
                    "south",
                    "west",
                    "north",
                    "east"
                );
            
            document.addEventListener("deviceready", onDeviceReady, false);
            
            // 初期時
            function onDeviceReady() {
                start_watch();
            }
            
            // コンパス起動
            function start_watch() {
                var options = { frequency: 500 };
                watchID = navigator.compass.watchHeading(onSuccess, onError, options);
            }

            // コンパス停止            
            function stop_watch() {
                if (watchID) {
                    navigator.compass.clearWatch(watchID);
                    watchID = null;
                }
            }
            
            // コンパスデータ取得
            function onSuccess(heading) {
                var element = document.getElementById('heading');
                element.innerHTML = '方位: ' + heading.magneticHeading + ' , ' + 
                                    '方角: ' + orient_string[toOrientationString(heading.magneticHeading)];
            }
            
            // コンパスデータ取得失敗
            function onError(e) {
                if (e.code == CompassError.COMPASS_INTERNAL_ERR) {
                    alert("内部エラーが発生しました");
                } else if (CompassError.COMPASS_NOT_SUPPORTED) {
                    alert("サポートしていません");
                } else {
                    alert("エラーが発生しました[" + e.code + "]")
                }
                stop_watch();
            }
            
            // 方位を取得
            function toOrientationString(angrad) {
                var idx = 0;
                
                for (i = 0; i < orient_range.length; i++) {
                    if (angrad < orient_range[i]) {
                        idx = i;
                        break;
                    }
                }
                
                return idx;
            }
        </script>
        <style>
            div, button {
                font-size: 24pt;
            }
        </style>
    </head>
    <body>
        <h1>コンパス</h1>
        <div id="heading">方位:取得中...</div>
        <button onclick="start_watch()">開始</button>
        <button onclick="stop_watch()">終了</button>
    </body>
</html>

方位を取得するのはnavigator.compass.watchHeadingの部分。
これは一定間隔で方位を取得してくれるもので引数は順に成功時イベントハンドラ、失敗時イベントハンドラ、間隔(ms)。

ネイティブアプリに比べるとものすごく楽。

これでAndroidの動作は確認することができました。
(iOSは手元にiPod touchしかなく動きませんでした。。。)

一番苦労したというかハマったことがGoogle検索。。。
Google検索でPhoneGapのドキュメントとか見てたら見てたやつがver1.0のもの。
おかげでなかなか動きませんでした。

ドキュメントは読む際はバージョンもしっかり確認しないとですね。
Apache Cordova API Document

※2012.6.30
ちょっと式が間違ってたんで修正しました。


Androidでセンサーを使って方位(方角)を取得する

昨日書いた文字列を描画するっていう前触れはどこにいったのか。。。
今日はセンサーを使ってみました。

使ったセンサーは地磁気センサーと加速度センサー。
方位センサーっていうのがもともとTYPE_ORIENTATIONというのであったみたいなんですが今は非推奨。
ですのでこの2つのセンサーを利用して方位を取得します。

まずセンサーを利用する準備。
public class CompassTestActivity extends Activity
    implements SensorEventListener {

    private SensorManager sensor;

    /** lifecycle */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        Window window = getWindow();
        window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
        window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        
        sensor = (SensorManager)getSystemService(SENSOR_SERVICE);
        
        setContentView(R.layout.main);
    }
    
    @Override
    public void onResume() {
     super.onResume();
     
     // センサーイベントの登録
        sensor.registerListener(this, sensor.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD), SensorManager.SENSOR_DELAY_UI);
        sensor.registerListener(this, sensor.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), SensorManager.SENSOR_DELAY_UI);
    }
    
    @Override
    public void onPause() {
        // センサーイベントを削除
        sensor.unregisterListener(this);
     
        super.onPause();
    }
}

sensor.registerListenerの引数は

  • イベントリスナーのオブジェクト
  • センサーの種類
  • データ取得の頻度
といったかんじになります。
頻度をSENSOR_DELAY_UIとしてますが他にもSENSOR_DELAY_NORMALとかSENSOR_DELAY_GAMEなどいくつかありますのでつくるアプリ合わせて設定をします。

センサーは(センサーに限った話しではないですが)アプリがバックグランドになったときも動き続けますのでonResumeでイベント登録し、onPauseでイベント削除します。


つぎにセンサーイベントの実装です。
    /** SensorEventListener */
    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) { }

    @Override
    public void onSensorChanged(SensorEvent event) {
        if (event.accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE)
            return;

        switch (event.sensor.getType()) {
            case Sensor.TYPE_MAGNETIC_FIELD: // 地磁気センサ
                magneticValues = event.values.clone();
                break;
            case Sensor.TYPE_ACCELEROMETER:  // 加速度センサ
                accelerometerValues = event.values.clone();
                break;
        }

        if (magneticValues != null && accelerometerValues != null) {
            float[] rotationMatrix = new float[MATRIX_SIZE];
            float[] inclinationMatrix = new float[MATRIX_SIZE];
            float[] remapedMatrix = new float[MATRIX_SIZE];

            float[] orientationValues = new float[DIMENSION];

            // 加速度センサと地磁気センサから回転行列を取得
            SensorManager.getRotationMatrix(rotationMatrix, inclinationMatrix, accelerometerValues, magneticValues);
            SensorManager.remapCoordinateSystem(rotationMatrix, SensorManager.AXIS_X, SensorManager.AXIS_Z, remapedMatrix);
            SensorManager.getOrientation(remapedMatrix, orientationValues);

            // 方位を取得する
            int orientDegrees = toOrientationDegrees(orientationValues[0]);
            String orientString = toOrientationString(orientationValues[0]);

            Log.d(TAG, "orientDegrees = " + orientDegrees + " , orientString = " + orientString);
        }
    }
 
    /**
     * 方位の角度に変換する
     * @param angrad
     * @return
     */
    private int toOrientationDegrees(float angrad) {
        return (int)Math.floor(angrad >= 0 ? Math.toDegrees(angrad) : 360 + Math.toDegrees(angrad));
    }

    /**
     * 方位の文字列に変換する
     * @param angrad
     * @return
     */
    private String toOrientationString(float angrad) {
        double[] orientation_range = {
            - (Math.PI * 3 / 4), // 南
            - (Math.PI * 1 / 4), // 西
            + (Math.PI * 1 / 4), // 北
            + (Math.PI * 3 / 4), // 東
        };

        String[] orientation_string = {
            "south",
            "west",
            "north",
            "east",
        };

        for (int i = 0; i < orientation_range.length; i++) {
            if (angrad < orientation_range[i]) {
                return orientation_string[i];
            }
        }

        return orientation_string[0];
    }

まだまだわからないことがありますがセンサーイベントには、onAccuracyChangedとonSensorChangedがあります。

  • onAccuracyChanged・・・精度?に変化があった場合
  • onSensorChanged・・・値に変化があった場合
ここでは値に変化があった場合に処理するようにしています。
中身はあんまり詳しく説明できないので参考にしたサイト
このセンサーとカメラ機能を使っていま向いてる方位(4方位しかないですが)を表示するだけのものを作ってみました。

ただ、Galaxy Nexusでは動きませんでした。。。なんでだ。。。
動作はAQUOS PHONE f SH-13Cで確認しました




2012年6月28日木曜日

文字列を描画(オーバーレイ)する

いろいろ考えてたけど基本的なところからこつこつやろうと。。。
基本的なところですが文字列を描画するやり方。

ただ描画するだけではもの足りないかな思ってオーバーレイしてみましたがまだもの足りません。

とりあえずソースはこんなかんじ
package jp.kuseful.drawtext;

import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup.LayoutParams;

public class DrawTextActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        // オーバーレイするViewを追加する
        OverlayView overlay = new OverlayView(this);
        addContentView(overlay, new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
    }
    
    public class OverlayView extends View {

                public OverlayView(Context context) {
                    super(context);
                }

                @Override
                protected void onDraw(Canvas canvas) {
                        // Paintオブジェクト生成
                        Paint paint = new Paint();
                        paint.setColor(Color.WHITE);
                        paint.setTextSize(30);

                        // canvasに描画
                        canvas.drawText("OverlayText", 100, 100, paint);
                }
    }
}

これでOK。

今回はオーバーレイですがViewを差し替える場合は
setContentView(new OverlayView(this))
こうなります。



2012年6月27日水曜日

AndroidでQRコードを読み込む(ZXing 2.0)

今日はAndroidをさわってました。
(毎日何をやってるのかだんだんわからなくなってきた。。。)

AndroidでQRコードを使う場合、だいたいがZXing(ゼブラクロッシング)を利用してアプリを構築しているみたいです。
今日はカメラ上からQRコードを読み込んで読み込んだ内容を表示するという単純なものでしたがなかなかすんなりいかず苦労したんでメモ程度に。

とりあえずライブラリをダウンロードします。(現在の最新はZXing-2.0.zip)
http://code.google.com/p/zxing/downloads/list

ダウンロードしたファイルを解凍するといろいろ入ってますがアプリ作るのに必要なのは以下

  • core/core.jar
  • javase/javase.jar
  • android/src/com/google/zxing/client/android/PlanarYUVLuminanceSource.java

まずAndroidのプロジェクトを作成。(ここではZXingTestとしました)
続けて「libs」フォルダと「com.google.zxing.client.android」パッケージを作成します。
libsには「core.jar」と「javase.jar」、パッケージには「PlanarYUVLuminanceSource.java」をコピーします。
libsはこのままでは使えないのでそれぞれ右クリックしてビルド・パスに追加をしておきます。
追加後のツリーはこんなかんじです。



次にメインの画面です。(res/layout/main.xml)
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
    
    <SurfaceView
        android:id="@+id/preview_view"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />
    
    <RelativeLayout
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" >
        
        <View
            android:id="@+id/center_view"
            android:layout_width="300dip"
            android:layout_height="180dip"
            android:layout_centerVertical="true"
            android:layout_centerHorizontal="true"
            android:background="#55ff6666" />
        
    </RelativeLayout>
    
</FrameLayout>

SurfaceViewはカメラを使うんでお決まりですかね。
Viewにしてる部分はQRコードを読み込むエリアを描画しています。

次にカメラを使うのでマニフェストファイルを修正します。(AndroidManifest.xml)
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="jp.kuseful.zxingtest"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="8" />
    
    <uses-permission android:name="android.permission.CAMERA" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".ZXingTestActivity"
            android:label="@string/app_name"
            android:screenOrientation="landscape" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

パーミッションの追加とあとアプリを横向きとするためscreenOrientationをlandscapeに指定しています。

次にメインのソース部分です。(ZXingTextActivity.java)
package jp.kuseful.zxingtest;

import java.io.IOException;

import com.google.zxing.BinaryBitmap;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.Result;
import com.google.zxing.client.android.PlanarYUVLuminanceSource;
import com.google.zxing.common.HybridBinarizer;

import android.app.Activity;
import android.content.Context;
import android.graphics.Point;
import android.hardware.Camera;
import android.os.Bundle;
import android.util.Log;
import android.view.Display;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;

public class ZXingTestActivity extends Activity
    implements SurfaceHolder.Callback, Camera.PreviewCallback, Camera.AutoFocusCallback {
    
    private static final String TAG = "ZXingTest";
    
    private static final int MIN_PREVIEW_PIXCELS = 320 * 240;
    private static final int MAX_PREVIEW_PIXCELS = 800 * 480;

    private Camera myCamera;
    private SurfaceView surfaceView;
    
    private Boolean hasSurface;    
    private Boolean initialized;
    
    private Point screenPoint;
    private Point previewPoint;
    
    /** lifecycle */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        Window window = getWindow();
        window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        
        hasSurface = false;
        initialized = false;
        
        setContentView(R.layout.main);
    }
    
    @Override
    protected void onResume() {
        super.onResume();
        
        surfaceView = (SurfaceView)findViewById(R.id.preview_view);
        SurfaceHolder holder = surfaceView.getHolder();
        if (hasSurface) {
            initCamera(holder);
        } else {
            holder.addCallback(this);
            holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
        }
    }
    
    @Override
    protected void onPause() {
        closeCamera();
        if (!hasSurface) {
            SurfaceHolder holder = surfaceView.getHolder();
            holder.removeCallback(this);
        }
        super.onPause();
    }

    /** SurfaceHolder.Callback */
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        if (!hasSurface) {
            hasSurface = true;
            initCamera(holder);
        }
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        hasSurface = false;
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    /** Camera.AutoFocusCallback */
    @Override
    public void onAutoFocus(boolean success, Camera camera) {
        if (success)
            camera.setOneShotPreviewCallback(this);
    }
    
    /** devices */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (myCamera != null) {
            if (event.getAction() == MotionEvent.ACTION_DOWN) {
                Camera.Parameters parameters = myCamera.getParameters();
                if (!parameters.getFocusMode().equals(Camera.Parameters.FOCUS_MODE_FIXED)) {
                    myCamera.autoFocus(this);
                }
            }
        }
        return true;
    }

    /** Camera.PreviewCallback */
    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        View centerView = (View)findViewById(R.id.center_view);
        
        int left = centerView.getLeft() * previewPoint.x / screenPoint.x;
        int top = centerView.getTop() * previewPoint.y / screenPoint.y;
        int width = centerView.getWidth() * previewPoint.x / screenPoint.x;
        int height = centerView.getHeight() * previewPoint.y / screenPoint.y;
        
        PlanarYUVLuminanceSource source    = new PlanarYUVLuminanceSource(
                                                    data,
                                                    previewPoint.x,
                                                    previewPoint.y,
                                                    left,
                                                    top,
                                                    width,
                                                    height,
                                                    false);
        
        BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
        MultiFormatReader reader = new MultiFormatReader();
        try {
            Result result = reader.decode(bitmap);
            Toast.makeText(this, result.getText(), Toast.LENGTH_LONG).show();
        } catch (Exception e) {
            Toast.makeText(this, "error: " + e.getMessage(), Toast.LENGTH_LONG).show();
        }
    }
    
    /**
     * カメラ情報を初期化
     * @param holder
     */
    private void initCamera(SurfaceHolder holder) {
        try {
            openCamera(holder);
        } catch (Exception e) {
            Log.w(TAG, e);
        }
    }
    
    private void openCamera(SurfaceHolder holder) throws IOException {
        if (myCamera == null) {
            myCamera = Camera.open();
            if (myCamera == null) {
                throw new IOException();
            }
        }
        myCamera.setPreviewDisplay(holder);
        
        if (!initialized) {
            initialized = true;
            initFromCameraParameters(myCamera);
        }
        
        setCameraParameters(myCamera);
        myCamera.startPreview();
    }
    
    /**
     * カメラ情報を破棄
     */
    private void closeCamera() {
        if (myCamera != null) {
            myCamera.stopPreview();
            myCamera.release();
            myCamera = null;
        }
    }
    
    /**
     * カメラ情報を設定
     * @param camera
     */
    private void setCameraParameters(Camera camera) {
        Camera.Parameters parameters = camera.getParameters();
        
        parameters.setPreviewSize(previewPoint.x, previewPoint.y);
        camera.setParameters(parameters);
    }
    
    /**
     * カメラのプレビューサイズ・画面サイズを設定
     * @param camera
     */
    private void initFromCameraParameters(Camera camera) {
        Camera.Parameters parameters = camera.getParameters();
        WindowManager manager = (WindowManager)getApplication().getSystemService(Context.WINDOW_SERVICE);
        Display display = manager.getDefaultDisplay();
        int width = display.getWidth();
        int height = display.getHeight();
        
        if (width < height) {
            int tmp = width;
            width = height;
            height = tmp;
        }
        
        screenPoint = new Point(width, height);
        Log.d(TAG, "screenPoint = " + screenPoint);
        previewPoint = findPreviewPoint(parameters, screenPoint, false);
        Log.d(TAG, "previewPoint = " + previewPoint);
    }
    
    /**
     * 最適なプレビューサイズを設定
     * @param parameters
     * @param screenPoint
     * @param portrait
     * @return
     */
    private Point findPreviewPoint(Camera.Parameters parameters, Point screenPoint, boolean portrait) {
        Point previewPoint = null;
        int diff = Integer.MAX_VALUE;
        
        for (Camera.Size supportPreviewSize : parameters.getSupportedPreviewSizes()) {
            int pixels = supportPreviewSize.width * supportPreviewSize.height;
            if (pixels < MIN_PREVIEW_PIXCELS || pixels > MAX_PREVIEW_PIXCELS) {
                continue;
            }
            
            int supportedWidth = portrait ? supportPreviewSize.height : supportPreviewSize.width;
            int supportedHeight = portrait ? supportPreviewSize.width : supportPreviewSize.height;
            int newDiff = Math.abs(screenPoint.x * supportedHeight - supportedWidth * screenPoint.y);
            
            if (newDiff == 0) {
                previewPoint = new Point(supportedWidth, supportedHeight);
                break;
            }
            
            if (newDiff < diff) {
                previewPoint = new Point(supportedWidth, supportedHeight);
                diff = newDiff;
            }
        }
        if (previewPoint == null) {
            Camera.Size defaultPreviewSize = parameters.getPreviewSize();
            previewPoint = new Point(defaultPreviewSize.width, defaultPreviewSize.height);
        }
        
        return previewPoint;
    }
}

コメント書いてないからわかりづらいですね。すみません。。。
動作としては起動後にカメラプレビューを表示し、画面タッチをした際にオートフォーカス実装後、QRコードの読み込みを開始する。
といったかんじです。
肝となるのはPlanarYUVLuminanceSource。
引数は順に、byte配列の画像データ、プレビューサイズの横幅、プレビューサイズの高さ、読み取りエリアの開始X座標、読み取りエリアの開始Y座標、読み取りエリアの横幅、読み取りエリアの高さ、反転の有無です。

正常に読み込めた場合にはこんなかんじで表示されます。


iOSのほうも試してみました。
http://teru2-bo2.blogspot.jp/2012/07/iosqrzxing-20.html


2012年6月26日火曜日

Node.js+expressをとりあえず試してみた

朝からインストールをしたNode.jsですがいろいろ見ていてもしょうがないので
とりあえず試しに動かしてみました。

Node.js恒例?のチャットアプリ。

Socket.IOを使うのかなって思ってましたがexpressでできそうなのでまずそれをインストールします。
$ npm install express

次にexpressには最低限必要となるファイル等があるみたいです。
それらを生成する必要がありますが自動的に生成してくれるコマンドがあります。
$ node node_modules/express/bin/express chatsample

こうすることでchatsampleというディレクトリが生成され、実行環境が整います。
さらにもうひとつ。expressに依存するモジュールもついでにインストールします。
$ cd chatsample
$ npm install

これでOK。

とりあえず動かしましょう。http://127.0.0.1:3000/
$ node app.js

こんな画面がでればOK。
中身を見てみるとjadeというNode用?のテンプレートエンジンを使って表示されているのがわかります。
とりあえずややこしいのでここでは使いませんが。。。

これからチャットアプリを作成していくわけですが、初心者なんでほとんどサンプルのままです。。。

app.js
var express = require('express')
  , routes = require('./routes')
  , http = require('http');

var app = express();

app.configure(function(){
    app.set('port', process.env.PORT || 3000);
    app.set('views', __dirname + '/views');
    app.set('view engine', 'jade');
    app.use(express.favicon());
    app.use(express.logger('dev'));
    app.use(express.bodyParser());
    app.use(express.methodOverride());
    app.use(app.router);
    app.use(express.static(__dirname + '/public'));
});

app.configure('development', function(){
    app.use(express.errorHandler());
});

// メッセージを管理するオブジェクト
var messageStore = {
    _messages: [],
    _listeners: [],

    // メッセージを追加
    addMessage: function(entry) {
        this._messages.push(entry);
        if (this._messages.length > 20) {
            this._messages = this._messages.slice(1);
        }

        for (var key in this._listeners) {
            this._listeners[key]();
            delete this._listeners[key];
        }
    },

    // メッセージを受け取ったある通知するコールバックを登録
    listenOnce: function(listener) {
        this._listeners.push(listener);
    },

    // メッセージを取得
    getMessages: function() {
        return this._messages;
    }
};

app.get("/", routes.index);

// メッセージの読み込みを受け付ける
app.get("/message", function(req, res) {
    res.header("content-type", "text/javascript");

    if (req.url == "/message") {
        // すぐにメッセージを返す
        res.end(JSON.stringify(messageStore.getMessages()));
    } else {
        // 新しいメッセージがくるまで待つ
        messageStore.listenOnce(function() {
            res.end(JSON.stringify(messageStore.getMessages()));
        });
    }
});

// メッセージの書き込みを受け付ける
app.post("/message", function(req, res) {
    var entry = {
        name: req.body.name,
        message: req.body.message,
    };

    messageStore.addMessage(entry);

    res.status = 202;
    res.send("message received");
});

http.createServer(app).listen(app.get('port'), function(){
    console.log("Express server listening on port " + app.get('port'));
});

次に画面です。public/chat.html
<!DOCTYPE HTML>
<html>

<head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="/stylesheets/style.css">
    <title>Chat with Node.js</title>
</head>

<body>
<h1>Chat with Node.js</h1>

<form id="form" action="">
    <input type="text" name="name" id="name" size="10">
    <input type="text" name="message" id="message" size="60">
    <input type="submit" value="send" id="send">
</form>

<hr>
<div id="chat"></div>

<script type="text/javascript" src="/javascripts/client.js"></script>

</body>

</html>

最後にクライアント側のJS。public/javascripts/client.js
var connect = window.addEventListener
                ? function(element, type, callback) { // IE以外
                    element.addEventListener(type, callback, false);
                }
                : function(element, type, callback) { // IE
                    element.attachEvent("on" + type, function() {
                        return callback(window.event);
                    });
                };

var createXHR = XMLHttpRequest
                ? function() {
                    return new XMLHttpRequest();
                }
                : function() {
                    return new ActiveXObject("MSXML2.XMLHTTP.6.0");
                }

// ページが読み込まれたら呼び出される
connect(window, "load", function() {
    var nameInput = document.getElementById('name'),
        messageInput = document.getElementById('message'),
        sendMessage = function() {
            var data = "name=" + encodeURIComponent(nameInput.value)
                        + "&message=" + encodeURIComponent(messageInput.value),
                    xhr = createXHR();

            xhr.open("POST", "message", false);
            xhr.setRequestHeader("content-type", "application/x-www-form-urlencoded");
            xhr.send(data);
        };

    var f = document.getElementById('form');

    connect(f, "submit", function(event) {
        sendMessage();
        messageInput.value = "";
        if (event.preventDefault) {
            event.preventDefault();
        }

        return false;
    });

    loop(true);
});

// メッセージを受け取るループ処理
function loop(isFirst) {
    var xhr = createXHR(),
        query = isFirst ? "" : ("?" + new Date().getTime());

    xhr.open("GET", "message" + query, true);
    xhr.onreadystatechange = function() {
        if (xhr.readyState == 4) {
            if (xhr.status == 200) {
                render(eval(xhr.responseText));
            }

            loop(false);
        }
    };
    xhr.send();
}

// 受け取ったメッセージを表示する
function render(log) {
    var chat = document.getElementById('chat');

    chat.innerHTML = " ";
    log.reverse();

    for (var key in log) {
        var entry = log[key],
            div = document.createElement('div'),
            message = document.createTextNode(entry['name'] + ": " + entry['message']);

        div.appendChild(message);
        chat.appendChild(div);
    }
}

ほぼはじめてさわったので説明が難しいですがこれで動作します。
複数のブウラザからhttp://127.0.0.1:3000/chat.htmlにアクセスして見てください。

従来のWebアプリケーションと違い画面のリロードとか定期的にサーバへアクセスするなど余計なものがないはわかります。
がなかなか動きを把握するのにて手間取りました。

jQueryとかSocket.IOとかを使えばもっとシンプルに書けそうな気もするんですがとりあえずはこんなかんじで。



Macにnode.jsをインストール

前にさわったようなさわってないようなnode.js。
http://nodejs.org/

WebSocketを試してみたくて手軽に試せそうでインストールしてみました。
インストール手順をメモ程度に。。。

$ curl -O http://nodejs.org/dist/node-latest.tar.gz
$ tar zxvf node-latest.tar.gz
$ cd node-v0.8.0
$ ./configure
$ make
$ sudo make install
$ node -v
v0.8.0

インストールはこれでOK。

とりあえずお決まりのHelloWorldを実行してみます。
適当なディレクトリにファイルを作成します。

HelloWorld.js
var http = require('http');

var server = http.createServer(
 function(request, response) {
  response.writeHead(200, {'content-type': 'text/plain'});
  response.end('Hello World!');
 }
).listen(8124);

console.log('Server running at http://127.0.0.1:8124/')

これができたらターミナルでnode.jsを動かします。
$ node HelloWorld.js
Server running at http://127.0.0.1:8124

ブラウザを起動してhttp://127.0.0.1:8124にアクセスしてHelloWorldが表示されればOK。

簡単です。

ついでにnpm(node package manager)もインストールしておきました。
私の環境(Mac OS Lion)では権限を切り替えてからしか正常にインストールできませんでした。
$ sudo -s
# curl https://npmjs.org/install.sh | sh
# exit
$ npm -v
1.1.32

これでOK。
もうひとつついでにnpmの動作確認もかねてsocket.ioもインストール
$ npm install socket.io

これで一通りの準備はできたと思います。
また時間があるときにいろいろ作っていきたいと思います。


2012年6月23日土曜日

Monacaに期待

昨日なぜいきなりPhoneGapを久しぶりに触ってみたかというと実はこれが気になって。。。

Monaca
http://monaca.mobi/

今はまだBeta版ですが

  • クラウド上でiOS/androidの開発が同時できる
  • コアとなる部分にPhoneGapを採用している
  • ブラウザIDEのため、開発環境を揃える必要がない


先日はPhoneGapを使ったiOS/androidの記事を書きましたがこのMonaca
同時に開発できます。

もともとPhoneGap自体が同じコードでiOSとAndroidの開発ができる点が強みだと思うんですがいくら同じコードでも環境が違えばどうしてもその分手間が増えます。
(AndroidならEclipse、iOSならXcodeとか)


このMonacaでは同じコードの部分、つまりindex.htmlの部分だけしか記述する必要がありません。
(深く使っていけばAndroidManifest.xmlやplistなど多少の違いはありますが。。。)

PhoneGapを使ってiOS/Androidと同時に対応を考えているという方にはかなり利点が多いサービスではないかなと思います。

デバッグやインストールも専用アプリが必要となりますが日本語チュートリアルが丁寧に書かれてますし登録も無料なんでそれほど苦労せずに試すことができるかと思います。

自分もまだまだ使ってる時間少ないんでわかりませんが今後期待できそうな気がします!!!







2012年6月22日金曜日

PhoneGapを再び試してみました(Android)

次はAndroidです。
iOSはこちら。http://teru2-bo2.blogspot.jp/2012/06/phonegapios.html

iOSの場合はパッケージをインストールしましたがAndroidの場合は、インストール不要です。
不要ですが。。。これまたちょっとした手間があります。

まず、Eclipseを起動して新規にAndroidプロジェクトを作成します。
パッケージエクスプローラを開いて「libs」と「assets/www」フォルダを作成します。

次にPhoneGapのサイトからダウンロードしたファイルを展開しそれぞれコピーを行います。

  • libs/cordova-1.8.1.jar
  • assets/www/cordova-1.8.1.js
jarファイルはこのままでは参照されないのでcordova-1.8.1.jarを右クリックして「ビルドパスに追加」をします。

このあたりがちょっと手間ですが、決まったことなんで慣れかなと。
これで準備は完了です。

次に画面となるファイルを作成します。場所は「assets/www/index.html」です。
ソースファイルはiOSのときと同じです。
<!DOCTYPE HTML>
<html>
    
    <head>
        <meta charset="utf-8">
            <title>カメラサンプル</title>
            <script type="text/javascript" charset="utf-8" src="cordova-1.8.1.js"></script>
            <script type="text/javascript" charset="utf-8">
                var pictureSource;
                var destinationType;
                
                document.addEventListener("deviceready", onDeviceReady, false);
                
                function onDeviceReady() {
                    pictureSource = navigator.camera.PictureSourceType;
                    destinationType = navigator.camera.DestinationType;
                }
                
                function onPhotoDataSuccess(imageData) {
                    smallImage = document.getElementById('smallImage');
                    smallImage.style.display = "block";
                    smallImage.src = "data:image/jpeg;base64," + imageData;
                }
                function onPhotoURISuccess(imageURI) {
                    largeImage = document.getElementById('largeImage');
                    largeImage.style.display = "block";
                    largeImage.src = imageURI;   
                }
                function onFail(message) {
                    alert(message);
                }
                
                function capturePhoto() {
                    navigator.camera.getPicture(onPhotoDataSuccess, onFail, {
                                                quality: 50,
                                                destinationType: destinationType.DATA_URL
                                                });
                }
                function capturePhotoEdit() {
                    navigator.camera.getPicture(onPhotoDataSuccess, onFail, {
                                                quality: 20,
                                                destinationType: destinationType.DATA_URL,
                                                allowEdit: true
                                                });
                }
                function getPhoto(source) {
                    navigator.camera.getPicture(onPhotoURISuccess, onFail, {
                                                quality: 50,
                                                destinationType: destinationType.FILE_URI,
                                                sourceType: source
                                                });
                }
                </script>
    </head>
    
    <body>
        <button onclick="capturePhoto();">Capture Photo</button>
        <button onclick="capturePhotoEdit();">Capture Editable Photo</button>
        <button onclick="getPhoto(pictureSource.PHOTOLIBRARY);">From Photo Library</button>
        <button onclick="getPhoto(pictureSource.SAVEDPHOTOALBUM);">From Photo Album</button>
        <img src="" id="smallImage" style="display:none;width:60px;height:60px;">
            <img src="" id="largeImage">
    </body>
    
</html>

これでOK。ではありません。
Android側ではメインのアクティビティとマニフェストファイルを修正する必要があります。

CameraPhoneGapActivity.java
package jp.kuseful.cameraphonegap;

import org.apache.cordova.DroidGap;

import android.os.Bundle;

public class CameraPhoneGapActivity extends DroidGap {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        super.loadUrl("file:///android_asset/www/index.html");
    }
}

ネイティブアプリはActivityを継承しますがPhoneGapではDroidGapを継承します。
また画面を表示するのにsetContentView()ではなくsuper.loadUrl()を使用します。

次にAndroidManifest.xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

パーミッションの宣言ですね。
カメラを使うんでCameraとかWRITE_EXTERNAL_STORAGEはいいんですが
私の環境のせい??
ACCESS_NETWORK_STATEは必須でした。
(カメラとかに関係なく)

これでOK。
実機で試してみてください。




PhoneGapを再び試してみました(iOS)

ちょうど一年ほど前にPhoneGapを試してみましたが
当時のバージョンは0.9.5.1。。。
PhoneGapを試してみた

現在の最新版は1.8.1。
一年でここまでバージョンあがるんだなって思いながら試してみました。
一年前はほとんど何も書いてなかったんで今回は簡単にカメラ撮影のアプリを試してみました。

とりあえず最新のパッケージをダウンロードします。http://phonegap.com/

iOSの場合はパッケージをインストールするみたいですね。
ダウンロードしたファイルを展開して「lib/ios/Cordova-1.8.1.dmg」をインストールします。
これでインストールは完了。

実際に作成する場合はネイティブアプリ同様、Xcodeを使います。
起動して「Create a new Xcode Projects」を選択するとテンプレート画面が表示されます。ここで「Cordova-based Application」を選択します。

選択後、「Next」でプロジェクト名を入力します。
(ここでは「CameraPhoneGap」としました)

プロジェクトが作成できたらとりあえず実行!とすると警告が表示されます。
アプリの方もエラーがでています。


「www」フォルダがありません。
と言った内容なんで参照するようにしますが、実はこのエラー、けっこう重要?で一度ビルドすると勝手に生成されます。
実際にできるものとしては「www/cordova-1.8.1.js」と「www/index.html」
確かに実行前にはjsファイルが見当たらなかった。。。

とりあえずFinderを開いてプロジェクトのパスにいくと「www」フォルダがあると思いますのでこれを参照するようにします。

これで再度、実行すると正常に実行されると思います。

これでOK。

これからカメラ機能を実装していきますが、これはPhoneGapのチュートリアルにあるものをそのまま使いましたので説明は省きますがソースだけ載せておきます。
<!DOCTYPE HTML>
<html>
    
    <head>
        <meta charset="utf-8">
            <title>カメラサンプル</title>
            <script type="text/javascript" charset="utf-8" src="cordova-1.8.1.js"></script>
            <script type="text/javascript" charset="utf-8">
                var pictureSource;
                var destinationType;
                
                document.addEventListener("deviceready", onDeviceReady, false);
                
                function onDeviceReady() {
                    pictureSource = navigator.camera.PictureSourceType;
                    destinationType = navigator.camera.DestinationType;
                }
                
                function onPhotoDataSuccess(imageData) {
                    smallImage = document.getElementById('smallImage');
                    smallImage.style.display = "block";
                    smallImage.src = "data:image/jpeg;base64," + imageData;
                }
                function onPhotoURISuccess(imageURI) {
                    largeImage = document.getElementById('largeImage');
                    largeImage.style.display = "block";
                    largeImage.src = imageURI;   
                }
                function onFail(message) {
                    alert(message);
                }
                
                function capturePhoto() {
                    navigator.camera.getPicture(onPhotoDataSuccess, onFail, {
                                                quality: 50,
                                                destinationType: destinationType.DATA_URL
                                                });
                }
                function capturePhotoEdit() {
                    navigator.camera.getPicture(onPhotoDataSuccess, onFail, {
                                                quality: 20,
                                                destinationType: destinationType.DATA_URL,
                                                allowEdit: true
                                                });
                }
                function getPhoto(source) {
                    navigator.camera.getPicture(onPhotoURISuccess, onFail, {
                                                quality: 50,
                                                destinationType: destinationType.FILE_URI,
                                                sourceType: source
                                                });
                }
                </script>
    </head>
    
    <body>
        <button onclick="capturePhoto();">Capture Photo</button>
        <button onclick="capturePhotoEdit();">Capture Editable Photo</button>
        <button onclick="getPhoto(pictureSource.PHOTOLIBRARY);">From Photo Library</button>
        <button onclick="getPhoto(pictureSource.SAVEDPHOTOALBUM);">From Photo Album</button>
        <img src="" id="smallImage" style="display:none;width:60px;height:60px;">
        <img src="" id="largeImage">
    </body>
    
</html>

これでOK。

Androidのほうも書いたんでそちらも。http://teru2-bo2.blogspot.jp/2012/06/phonegapandroid.html


2012年6月20日水曜日

FuelPHPでメール送信(smtp)

FuelPHPでメール送信(smtp)を試してみました。
やったことをメモ程度に。。。

まず、App/config/config.phpのpackagesにemailを追加
'packages' => array (
    'email',
);

次にpackages/email/config/email.phpをApp/config/にコピーします。
ここでメールの設定。
(メールサーバとか文字コードとかドライバとか)

実際にメール送信するコードは以下
    $email = Email::forge();
//    $email->from('test@example.com', mb_encode_mimeheader('テストユーザ', 'jis'));
    $email->from('test@example.com', 'テストユーザ');
    $email->to('hayashida@local.net');
//    $email->subject(mb_encode_mimeheader('テストメール', 'jis'));
    $email->subject('テストメール');
    $email->body(mb_convert_encoding($body, 'jis'));

    try
    {
        $email->send();
        $message = '送信しました';
    }
    catch (\EmailValidationFailedException $e)
    {
        $message = '送信に失敗しました。\n送信先のメールアドレスが正しくありません';
    }
    catch (\Exception $e)
    {
        $message = '送信に失敗しました' . $e->getMessage();
    }

こんなかんじでメール送信するみたいです。

ただ。。。開発環境で使ってるメールサーバ(WindowsにBlackJumboDog Ver4.2.3)でなかなか送信できませんでした。
原因としては「EHLO」コマンドにBlackJumboDogが対応していない。。。

「HELO」変えれないのかなって思ってFuelPHPのほうソース眺めてたらプログラム上は「EHLO」に失敗したら「HELO」を再送するようになってる。。。

よく見るとコマンド失敗したらエラーコードが返ってきたときにthrowされてるみたいで。。。バグ???

とりあえずpackages/email/classes/email/driver/smtp.php(行111目あたり)を以下のように修正。
    // Just say hello!
//    if($this->smtp_send('EHLO'.' '.\Input::server('SERVER_NAME', 'localhost.local'), 250, true) !== 250)
//    {
//        // Didn't work? Try HELO
//        $this->smtp_send('HELO'.' '.\Input::server('SERVER_NAME', 'localhost.local'), 250);
//    }
//
//    try
//    {
//        $this->smtp_send('HELP', 214);
//    }
//    catch(\SmtpCommandFailureException $e)
//    {
//        // Let this pass as some servers don't support this.
//    }
    try
    {
        try
        {
            $this->smtp_send('EHLO'.' '.\Input::server('SERVER_NAME', 'localhost.local'), 250);
        }
        catch(\SmtpCommandFailureException $e)
        {
            $this->smtp_send('HELO'.' '.\Input::server('SERVER_NAME', 'localhost.local'), 250);
        }
 
//    $this->smtp_send('HELP', 214);
    }
    catch (Exception $e)
    {
        throw $e;
    }

throwされてるからcatchしただけなんですが。。。
とりあえず動きました。
(もっといい方法があるかもしれないけど)

このあたり誰か詳しい方いたら教えて頂ければ助かります。

※2012.6.21
FuelPHP1.2からはメール送信時のmb_encode_mimeheaderは不要みたいです。
コード修正しました。

※2012.6.26
FuelPHP 1.2.1でバグ修正があったみたいです。




2012年6月19日火曜日

PHPでXMLを出力する

データベースを利用できない環境でデータをどう保持していこうか。
ありきたりかもしれませんがXMLかなと。。。

PHPではSimpleXMLを使うかDOMを使うか悩みそうなところですが
今回はDOMを触ってみました。
いくつかやったことをメモ程度に。

XMLファイルを生成する
// ファイル名
$filename = 'sample.xml';

// Domを生成
$dom = new DomDocument('1.0', 'utf-8');
$dom->formatOutput = true;

// 元となる要素を生成
$root = $dom->appendChild($dom->createElement('root'));
$head = $root->appendChild($dom->createElement('head'));
$body = $root->appendChild($dom->createElement('body'));

// ヘッダ要素を生成
$head->appendChild($dom->createElement('title', 'データファイル'));
$head->appendChild($dom->createElement('description', '説明'));

// ボディ要素を生成
// データ1
$content = $body->appendChild($dom->createElement('content'));
$content->appendChild($dom->createElement('name', '山田太郎'));
$content->appendChild($dom->createElement('age', '31'));
$content->appendChild($dom->createElement('gender', '男'));

// データ2
$content = $body->appendChild($dom->createElement('content'));
$content->appendChild($dom->createElement('name', '山田花子'));
$content->appendChild($dom->createElement('age', '28'));
$content->appendChild($dom->createElement('gender', '女'));

$dom->save($filename);

生成されたファイル
<?xml version="1.0" encoding="utf-8"?>
<root>
    <head>
        <title>データファイル</title>
        <description>説明</description>
    </head>
    <body>
        <content>
            <name>山田太郎</name>
            <age>31</age>
            <gender>男</gender>
        </content>
        <content>
            <name>山田花子</name>
            <age>28</age>
            <gender>女</gender>
       </content>
    </body>
</root>

次にXMLに要素を追加する
// ファイル名
$filename = 'sample.xml';

// ファイルを読み込む
$dom = new DomDocument('1.0', 'utf-8');
$dom->load($filename);

// 追加する場所を探す
$xpath = new DOMXPath($dom);
$xml = $xpath->query('/root/body');

// データ
$content = $xml->item(0)->appendChild($dom->createElement('content'));
$content->appendChild($dom->createElement('name', '鈴木一郎'));
$content->appendChild($dom->createElement('age', '38'));
$content->appendChild($dom->createElement('gender', '男'));

$dom->save($filename);

こんなかんじで。場所の指定の仕方がわからなくてなんか格好わるい。。。
SimpleXMLならもっとスマートにかけそうだけど。。。

XPathで複数の条件式を指定する
DOMXPathを使うことでXMLファイルからデータの検索とかができます。
追加のプログラムでは '/root/body' とだけ指定しました。
これはそのまま、'/root/body' 要素を取得します。
content要素を取得する場合は '/root/body/content' とすることで複数のcontent要素が抽出されます。

ここに条件式を記述することもできます。
'/root/body/content[gender="男"]'
とか
'/root/body/content[gender="男"][age=31]'
[ ] を複数記述すると and式 となるみたいです。
orは | です。

最後に基本的ですがノードの名前と値を取得する
$p->nodeName;
$p->nodeValue;

恥ずかしながらはじめこれに手間取ってなかなか進みませんでした。。。


今回はじめてDOMを操作してみましたがわりと面白かったんでもぉ少し勉強してみます。





2012年6月13日水曜日

PDOドライバのインストール

PDO(PHP Data Objects)

PDO自体はあらかじめ入ってたんですが各ドライバがなかったんでインストール方法を忘れないようにメモ。

まず、Linux環境にpdo_mysql。
$ cd /usr/local/src
$ pecl download pdo_mysql
$ tar xzf PDO_MYSQL-1.0.2.tgz
$ cd PDO_MYSQL-1.0.2
$ phpize
$ ./configure --with-pdo-mysql=/usr/local/mysql
$ make
$ sudo make install

これでpdo_mysql.soが生成されるのでphp.iniにpdo_mysql.soを追記。
(pdo_mysql.default_socketも設定した方が良いみたい)

次に、pdo_dblib(MSSQLにつなぐ用)。
$ cd /usr/local/src
$ pecl download pdo_dblib
$ tar xzf PDO_DBLIB-1.0.tar
$ cd PDO_DBLIB-1.0
$ phpize
$ ./configure --with-pdo-dblib=/usr/local/freetds
$ make
$ sudo make install

ただこれだとエラーがでます。
Cannot find FreeTDS in known installation directories

--with-pdo-dblibで指定したディレクトリにtds.hとlibtds.aがないとだめみたい。
ということでこんなかんじで作成
$ sudo touch /usr/local/freetds/include/tds.h
$ sudo touch /usr/local/freetds/lib/libtds.a

これで再度、makeするとOK。
php.iniにpdo_dblib.soを追記します。

次にWindowsですが。。。コメントを外すだけです。
ただし、MSSQLですがPHP5.3からphp_mssql.dll/php_pdo_mssql.dllがなくなってますんでsqlsrvを使う必要があります。
インストールとかは前に書いてたんで。。。
PHP5.3以降のMicrosoft SQL Server接続について


あとは使い方ですが以下
// MySQL(Linux・Windows)
$pdo = new PDO("mysql:host=<hostname>;dbname=<database>", "username", "password");

// MSSQL(Linux)
$pdo = new PDO("dblib:host=<hostname>;dbname=<database>", "username", "password");

// MSSQL(windows)
$pdo = new PDO("sqlsrv:server=<hostname>:database=<database>", "username", "password");

これで接続はできます。
MySQLはいいんですがMSSQLはLinuxとWindowsで中身が変わるのがちょっと気になるところ。



2012年6月12日火曜日

FuelPHPでpdo_dblibがつながらない?

Fuel\Core\Database_Exception [ Error ]: SQLSTATE[01002] Adaptive Server connection failed (severity 9)


PHPで普通に?PDOでMSSQLには繋がるけどFuelPHPを通すと繋がらない。。。
return array(
    'default' => array(
        'connection' => array(
            'dsn'        => 'dblib:host=<hostname>;dbname=<databasename>',
            'username'   => 'sa',
            'password'   => '',
        ),
    ),
);

db.phpはこんなかんじです。

繋がるソース↓
 $pdo = new PDO("dblib:host=<hostname>;dbname=<database>", "sa", "");

何が違うのか。。。

2012.6.13
動くようになりました。
アドバイスを頂いてなんとか動かすことができました。

まず、app/config/config.phpでlocaleを設定します。
return array(
 (省略)
  'locale' => 'ja_JP.utf8',
 (省略)
);

するとエラーメッセージが変わりました。
PDOException [ HY000 ]: SQLSTATE[HY000]: General error: 195 General SQL Server error: Check messages from the SQL Server [195] (severity 15) []

いろいろ調べてみるとMSSQL 2005/2008でset namesはサポートしてない?
(今回はMSSQL 2005で試してました)
データベース接続は初期値がutf8となっているのでこれを空にしてやることで回避できました。
これでクエリビルダを利用してSQLを実行すると動作が確認できました。
が、モデルを利用してfind_allで実行するとエラー。(`)が邪魔してうまく実行ができませんでした。

ただ、上の二つ(set namesの回避と(`))はdb.phpを修正することでエラー回避ができました。
return array(
    'default' => array(
        'connection' => array(
            'dsn'        => 'dblib:host=<hostname>;dbname=<databasename>',
            'username'   => 'sa',
            'password'   => '',
        ),
        'identifier' => '', // 空白を指定
        'charset'   => '', // 空白を指定
    ),
);

最終的にはこんなかんじのdb.phpとなりました。
とりあえず動いてよかったー。


2012年6月8日金曜日

mod_rewriteとurl rewrite

先ほどのサンプル(FuelPHPを試してみた)はとりあえず動かしたってかんじですが
URLに「public/index.php」がついてたんでこれを取り除いてhttp://localhost/fp-sampleでアクセスできるように。

これもドキュメントに載ってる通りそのままですが、ルートフォルダ(ここでは/fp-sampleの下)に.htaccessを作成して以下を記述します。
※.htaccessはpublicフォルダに入ってるんでそれを流用できます。
<IfModule mod_rewrite.c>
    RewriteEngine on
    RewriteBase /fp-sample/public
    RewriteRule ^(/)?$ index.php/$1 [L]
    RewriteCond ${REQUEST_FILENAME} !-f
    RewriteCond ${REQUEST_FILENAME} !-d
    RewriteRule ^(.*)?$ index.php/$1 [L]
</IfModule>

ドキュメントでは「RewriteBase /public」となっていますがここは環境に合わせて変更します。
これだけです。
本来であればpublicをウェブサーバのルートにおいてその他のファイルはルート外にするのが一般的みたいですがこういうやり方もあるみたいです。

ちなみにドキュメントにはApacheの環境しか載ってませんでしたがWindowsのIISでも同じようなことはできました。
※ただしIISにはmod_rewriteはないので別途URL Rewriteをインストールする必要があります。

IISの管理画面を開いてURL Rewriteの画面を開きます。
右のメニューから「Import Rules...」をクリックして以下の画面を開きます。
先に.htaccessファイルを作成していればそれをインポートすることもできます。
下のRewrite rulesに直接書き込んでも問題ありません。
(というかインポートするとここに入ります)
そのままだと上のようにエラーが表示されてしまいますので修正します。
RewriteEngine on
RewriteRule ^(/)?$ /fp-sample/public/index.php/$1 [L]
RewriteCond ${REQUEST_FILENAME} !-f
RewriteCond ${REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /fp-sample/public/index.php/$1 [L]

こんなかんじ。
ほとんど変わりませんがURL RewriteではRewriteBaseがサポートされていないみたいなんで上のような書き方で動きます。

IISで動かすとかいうのは稀でしょうが一応動いたんでメモ程度に。。。



FuelPHPを試してみた

FuelPHP
PHPの新しいフレームワークということでちょっとだけ触ってみました。
。。。なんとか動いたんでメモ程度に。

まず、サイトからバージョン1.2 をダウンロード。
ダウンロードしたファイルを展開してそのままウェブサーバに配置。
(今回はhtdocs > fp-sampleとしました)

とりあえずhttp://localhost/fp-sample/public/index.phpにアクセス。
「You don't have permission to access」と出たんで権限を与えます。
$ chmod 755 fp-sample

再度アクセスすると画面が表示されると思います。

はじめから動くサンプルとしてhttp://localhost/fp-sample/public/index.php/hello/にアクセス。画面に「Hello, World!」が表示されていると思います。
そのままアドレスの後ろに何か適当に文字を入力すると「Hello, <入力文字>」が表示されるようになると思います。

とまぁこんな感じです。
詳細はドキュメントを参考に。(まだ全然わかってないんで。)

もぉひとつ、データベースの接続を試してみました。

まず予めデータベースを作成しておきます。
mysql > create database fp_sample;

fuel > app > config > development > db.php を開いてデータベースの設定を行います。
<?php
return array(
    'default' => array(
        'type' => 'mysql',
        'connection'  => array(
            'hostname'   => 'localhost',
            'database'   => 'fp_sample',
            'username'   => 'root',
            'password'   => 'admin',
        ),
    ),
);

次にoilコマンドを使ってscaffoldでファイルを生成します。
ターミナルを起動してワークスペースに移動、以下を実行します。
$ oil generate scaffold post title:string summary:varchar[250] body:text

続けて以下を実行
$ oil refine migrate
Error: mysql_connect(): [2002] No such file or directory (trying to connect via unix:///var/mysql/mysql.sock)

ここでけっこう悩んでしまいました。
mysql.sockですが私の環境では/tmp/mysql.sockにあります。
phpinfo()で見ても/tmp/mysql.sockになってるし適当にfuelphpを使わずmysql_connectを実行すると普通に動くし。。。
悩んだあげく'socket' => '/tmp/mysql.sock'を追加することで実行されました。
<?php
return array(
    'default' => array(
        'type' => 'mysql',
        'connection'  => array(
            'hostname'   => 'localhost',
            'socket'     => '/tmp/mysql.sock',
            'database'   => 'fp_sample',
            'username'   => 'root',
            'password'   => 'admin',
        ),
    ),
);

これで再度migrateを実行すると成功しました。
$ oil refine migrate
Performed migrations for app:default:

----------------------
※kenji_sさんに教えて頂きましたがFuelPHPのトラブルシューティングに載っていました。
Oil がデータベースに接続できないがアプリケーションは接続できる
私の場合は'localhost'を'127.0.0.1'に変えるだけで動作しました。

----------------------



データベースを見るとpostsというテーブルが作成されていることが確認できると思います。
これでhttp://localhost/fp-sample/public/index.php/postにアクセス。
でかでかとエラーがでました。「Class 'Orm\Model' not found」
ormとかいうのを有効にしないとだめみたいです。
fuel > app > config > config.phpを修正します。
'always_load' => array(
    'packages' => array(
        'orm',
    ),
    (省略)
),

もともとコメントアウトさせているのでコメントを外すだけOK。
再度アクセス!
またエラー。「Crypto key error」。。。
よくわかりませんが「Please copy the following code into APPPATH/config/crypt.php manually:」とあるんで下のグレー部分をコピーしてcrypto.phpに貼付けろってかんじでしょう。
書かれてある通り fuel > app > config > crypt.phpを作って貼付け。

もっかいアクセス。
これでやっと画面が表示されました。

なかなか手間取りました。。。
手順が間違ってるのか?
けどとりあえずこれでデータの一覧〜登録、入力チェックまでを一気にやってくれます。

これからちょいちょい触っていきたいと思います。


2012年6月7日木曜日

EclipseのEgitでThe Current branch is not configured for pull

GitをEclipseから操作するためにEgitプラグラインを導入しています。

ローカルリポジトリの操作は問題なく動作するんですが
リモートの操作(共有リポジトリ)でちょっと苦戦しています。

基本的に開発する流れとしては
  1. ローカルリポジトリを作成
  2. 適当な具合に開発
  3. ローカルリポジトリにコミット
  4. 共有リポジトリにプッシュ
  5. 共有リポジトリからプル
となると思いますが、どうしても5のプルがうまくいかず下のようなエラーが発生してしまいました。
the current branch is not configured for pull novalue for key branch.master.merge found in configuration

branch.master.mergeが見つからない。的なエラー。
いろいろ調べてみたんですがEgitからの操作ではなくターミナルから操作を行い対処してます。
上の1〜3の操作は変わりませんが、4のプッシュする前にgit remote add とgit configを操作してます。
$ git remote add origin ssh://git@server/test.git
$ git config branch.master.remote origin
$ git config branch.master.merge refs/heads/master

これをターミナルで実行してプッシュ、プルすると成功しました。
(ターミナルだけでテストしてるときはこんなことしなくても動作したんだけどな。。。)

Egitを使う以上、しょうがないこと??
このあたり詳しい方いたら教えてください。

ちなみに共有リポジトリからcloneしてきた場合は、上のような操作は一切することなく動作しました。
(git config -lでconfigの中身を確認してみるとremoteもbranchも追加されていることがわかると思います)




2012年6月4日月曜日

post-updateでちょっとハマったこと

post-updateを使ってssh経由で共有リポジトリにpushしたとき、自動的にローカルリポジトリを更新したい場合などあると思います。

例)
Server A
    └ repo-sv.git (共有リポジトリ)
    └ repo (ローカルリポジトリ)

Client A
    └ repo (ローカルリポジトリ)


  1. Client A が repo (ローカルリポジトリ) で変更・コミット
  2. Client A が Server A の共有リポジトリへpush
  3. Server A の repo (ローカルリポジトリ) を自動的に更新


これを実現するために hooks/post-update を使用します。

ハマったんで流れを書いておきます。

まず Clien A でローカルリポジトリを作成・コミットします
$ mkdir repo
$ cd repo
$ git init
$ touch test.txt
$ git add .
$ git commit -m 'first'

次に Server A に共有リポジトリを作成します。
$ mkdir repo-sv.git
$ cd repo-sv.git
$ git init --bare --shared

次に Client A に戻り、共有リポジトリへpushします。
$ git remote add origin ssh://@/repo-sv.git
$ git push origin master

これでpushは完了です。
ただこれだと普通に共有リポジトリにpushしただけです。
自動的にローカルリポジトリを更新するため、hooks/post-updateを修正します。
$ cd hooks
$ cp post-update.sample post-update
$ vi post-update

// 中身はこんなかんじ
#!/bin/sh

cd /repo && git --git-dir=.git pull

// そのまま共有リポジトリからcloneしてローカルリポジトリを作成
$ cd /
$ git clone /repo-sv.git repo

// ファイルがあるか確認する
$ ls repo
test.txt

これで Server A にもローカルリポジトリができたことがわかると思います。
hooks/post-updateの処理も追加していますので Client A に戻って再度コミット・プッシュしてみましょう。
$ touch add.txt
$ git add .
$ git commit -m 'added'
$ git push origin master

Server A でローカルリポジトリ内を確認してみると先ほど追加した add.txt が作成されていると思います。
$ ls /repo
test.txt add.txt

これでOK。

自分がハマったのは Server A でローカルリポジトリを作成するとき、clone先をsshで指定していました。
一見問題ないように思えるんですが(実際にcloneもpushもpullもうまくいくと思います) Client A が push したとき、permissionのエラーがでてしまいました。

けっこうハマったけどとりあえず動いたんでまぁいいか。




2012年6月2日土曜日

WindowsにGitをインストール 其ノ二

次にGitとOpenSSHを使う設定関係をメモしときます。

インストールはこちら
http://teru2-bo2.blogspot.jp/2012/06/windowsgit.html

cygwinのインストールが完了したらCygwin Terminalを起動してGitが正しくインストールされているかを確認します。
$ git --version
git version 1.7.9

これでインストール完了しているのとバージョンの情報を確認することができます。
あとは任意のフォルダにリポジトリを作っていろいろな操作を試してみてください。

次にOpenSSHです。
ところでGitをするのになんでSSHサーバをたてる必要がある?というかんじですが。。。
共有リポジトリにアクセスするのに一番一般的な方法っぽかったから。。。
このあたりは人それぞれ考えがあると思いますので、一応マニュアルのリンクだけ貼っときます。
4.1 Gitサーバ プロトコル

OpenSSHの設定はちょっと私も専門外なんで参考にしたサイトのリンクを貼っときます。
けっこう詳細に書かれているのでわかりやすくすんなり設定することができました。
CygwinのOpenSSHを使って自宅のWindowsにアクセスする


かなり適当ですがこれでGitとOpenSSHの設定は完了です。
以下は私が試した例を書いておきます。これができればどちらも設定が完了しているとおもいます。
// ローカルリポジトリを作成・コミットまで
$ cd ~/
$ mkdir repo
$ cd repo
$ git init
$ touch test.txt
$ git add test.txt
$ git commit -m 'first commit'

// 共有リポジトリを作成
$ cd ~/
$ mkdir repo-sv.git
$ cd repo-sv.git
$ git init --bare --shared

// 共有リポジトリにpush
$ cd ~/
$ cd repo
$ git remote add origin ssh://127.0.0.1/home//repo-sv.git
$ git push origin master

プログラム組むのがメインなんでこのあたりどう説明していいかわかりませんが
基本的な動作はこれでできるようになると思います。





WindowsにGitをインストール 其の壱

windowsでGitをインストールする場合は、msysgitcygwinの二種類があります。
はじめはmsysgitで環境を作ろうと思ってたんですがなかなかうまくいかずcygwinでインストールしました。

一応、msysgitでもローカルリポジトリを作って操作したりLinuxで構築したGit環境(共有リポジトリ)へのpush・pullはできたんですが、Windowsで作った共有リポジトリにpushができませんでした。
msysgitというよりSSHサーバをFreeSSHdでたてたんですがその設定がうまくできませんでした。

なんか悔しいんでそのうちmsysgit+FreeSSHdもも一回試してみたいと思ってます。

とりあえず今回はcygwinを使ってGitとOpenSSHの環境を構築しました。

簡単ですがやったことをメモ程度に書いておきます。


1.cygwinのサイトから「setup.ext」をダウンロードします。

2.Cygwin Setupでは「次へ」をクリック

3.Choose Instation Typeは「Install from Internet」を選択して「次へ」をクリック

4.Choose Instation Directoryは「Root Directory」はそのまま、Install Forは「All User」を選択して「次へ」をクリック

5.Select Local Package Directoryは「C:\cygwin-dll」(何でも構いませんが)のように設定して「次へ」をクリック

6.Select Connection Typeは「Direct Connection」でも構いませんが「Use Internet Expoler Proxy Settings」を選択して「次へ」をクリック

7.Choose Download Siteは適当に任意のものを選択して「次へ」をクリック

8.インストールするパッケージを選択して「次へ」をクリック
今回私の場合はGitの環境を構築したかったので以下のパッケージを選択しました。
・Devel -> git
・Devel -> git-completion
・Devel -> git-gui
・Devel -> git-svn
・Devel -> subversion-perl
・Editors -> vim
・Net  -> openssh

9.しばらく待つとインストールが完了してデスクトップに「Cygwin Terminal」とかいうショートカットができると思います。



これでインストールは完了です。
長くなりそうなんで設定関係は別に載せます。
http://teru2-bo2.blogspot.jp/2012/06/windowsgit_02.html