2008-08-14

Picasa用写真アップロードiアプリの改善点

旅行先で使ってみると、それなりには使えたけどあまり実用的ではなかった。
特にGPSは計測までの手順が多く利便性が低い。
幾つか必要だと感じた修正点を上げると次のようになる。

・位置情報を保存できるようにして、位置情報の取得と投稿を分けて行えるようにする。
・複数の画像を投稿できるようにする。

この変を改良したいと思う。

2008-07-26

ブラウザからiアプリを起動

前回、iモードからGPSで計測した位置情報を取得した。
今回は、この情報を引数にブラウザからiアプリを起動しする。
iアプリ起動のためのHTMLは下記のようにを記述する。


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//i-mode group (ja)//DTD XHTML i-XHTML(Locale/Ver.=ja/2.1) 1.0//EN" "i-xhtml_4ja_10.dtd">
<html>
<head>
<title>iPicasa</title>
</head>
<body>
<div style="text-align: center">
<hr style="border-style: solid; border-color: #6AA7DB;" />
<object declare="declare" id="iPicasa.launch" data="JamのURL" type="application/x-jam" >
<param name="lat" value="緯度" />
<param name="lon" value="経度" />
</object>
<a ista="#iPicasa.launch" href="#" >iPicasaを起動</a>
<hr style="border-style: solid; border-color: #6AA7DB;" />
</div>
</body>
</html>


これを次のようにiアプリのソースに記述して、起動時に情報をする。

if (getLaunchType() == IApplication.LAUNCHED_FROM_BROWSER) {
lat = getParameter("lat");
lon = getParameter("lon");
}


これで画像のアップロード時に位置情報を付加することができる。

今回はあわせてiアプリのPOSTの制限である80Kバイトを超えてアップロードできるように以下のようにソースを修正した。iアプリのHTTPヘッダーに状態情報を付加し、プロキシーサーバーで結合してアップロードするようにした。その際、気をつけなくてはいけないのはiアプリでは独自のHTTPリクエストヘッダーを追加できないということ。例えばx-uploadというHTTPヘッダーを追加することはできない。そこでHTTPリクエストヘッダーのContent-Typeに独自に情報を付加することにした。


private void uploadPhoto() throws Exception {
try {
ImageStore imageStore;
try {
imageStore = ImageStore.getEntry(photoId);
} catch (StoreException e) {
throw new Exception("画像を選択してください");
}

waiting.start();

String[] contentType = {"Content-Type", ""};
String[][] requestPropertys = {contentType};


InputStream in = imageStore.getInputStream();
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte buff[] = new byte[1024];
int len;
while((len = in.read(buff)) != -1) {
if (out.size() + len > 80 * 1024) {
contentType[1] = "application/x-www-form-urlencoded; upload-status=continue";
String response = new String(post(proxyUrl+"/uploadPhoto.do", requestPropertys, out.toByteArray()));
if (!"OK".equals(response)) {
throw new Exception("アップロードに失敗しました");
}
out.close();
out = new ByteArrayOutputStream();
}
out.write(buff, 0, len);
}

if (out.size() > 0) {
contentType[1] = "application/x-www-form-urlencoded; upload-status=end";
String response = new String(post(proxyUrl+"/uploadPhoto.do", requestPropertys, out.toByteArray()));
if (!"OK".equals(response)) {
throw new Exception("アップロードに失敗しました");
}
out.close();
}
in.close();

} finally {
waiting.end();
}

repaint();
}

synchronized private byte [] post(String url, String[][] requestPropertys, byte [] data)
throws IOException {

HttpConnection conn = null;
OutputStream os = null;
InputStream is = null;
try {
conn = (HttpConnection)(Connector.open(url, Connector.READ_WRITE, true));
conn.setRequestMethod(HttpConnection.POST);

if (requestPropertys != null) {
for (int i=0; i < requestPropertys.length; i++) {
String[] requestProperty = requestPropertys[i];
conn.setRequestProperty(requestProperty[0], requestProperty[1]);
}
}

os = conn.openOutputStream();
os.write(data);
os.close();

conn.connect();

is = conn.openInputStream();

ByteArrayOutputStream response = new ByteArrayOutputStream();
byte buff[] = new byte[1024];
int len;
while((len = is.read(buff)) != -1) {
response.write(buff, 0, len);
}
is.close();

return response.toByteArray();

} finally {
try{
if (os != null) os.close();
if (is != null) is.close();
if (conn != null) conn.close();
} catch (IOException ioe) {}
}
}


今度は実際のこのiアプリを旅行先で使ってみたいと思う。

2008-07-13

写真の場所をGPSで計測して登録

GPS機能を使って写真の場所を計測できるようにしたい。
方法としては以下が考えられる。

1.測地情報を写真に設定し、写真を登録する。
2.測地情報を取得し、その測地情報と写真を登録する。

1についてはPicasa webで測地情報を表示してくれるので、わざわざ何することはない。(ただしPicasa webの設定変更は必要)
2については写真ごとに位置情報を設定しないでいいので、同じ場所で何度も写真を取る場合に効率が良く、こちらを実装してみようと思う。
ただiアプリでGPS機能を使うにはトラステッドiアプリでなければ使えないためiモードとの連携が必要。本当のところドコモには、ユーザー責任を確認のうえ、一般に使える機能として開放してほしい。

iモードからGPSを利用するサービスについては以下を参照

http://www.nttdocomo.co.jp/service/imode/make/content/gps/index.html

次はiモードから位置情報を取得するxhtml


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//i-mode group (ja)//DTD XHTML i-XHTML(Locale/Ver.=ja/2.1) 1.0//EN" "i-xhtml_4ja_10.dtd">
<html>
<head>
<title>iPicasa</title>
</head>
<body>
<div style="text-align: center">
<hr style="border-style: solid; border-color: #6AA7DB;" />
<a href="/setGeoLocation.do" lcs>現在地をGPSで計測</a>
<hr style="border-style: solid; border-color: #6AA7DB;" />
</div>
</body>
</html>



測地系はgoogleも、ドコモも、世界測地系(wgs84)で同じなのだが、ドコモでは、度は10進法でdd(00~90)、分と秒は60進法でmm、ssとそれぞれ表記し、秒の小数点以下の数値は10進法で三桁表記、googleは少数で表現している。このため、利用するには変換が必要。
以下がJavaによる変換のための関数の例。


private Double setLocation(String loc) {

StringTokenizer st = new StringTokenizer(loc, ".");

double d = Double.parseDouble(st.nextToken());
int sign = d > 0 ? 1 : -1;

d += (Double.parseDouble(st.nextToken())/60.0
+ Double.parseDouble(st.nextToken())/3600.0
+ Double.parseDouble(st.nextToken())/3600000.0)*sign;

return Double.valueOf(d);
}


次回はiアプリを修正して、測地情報を登録できるようにしよう。

2008-07-11

Picasa用アップローダーiアプリ

906シリーズからiモードにファイルのアップロード機能が追加になったので、きっとgoogleが
モバイル版Picasaにアップロード機能をつけてくれるだろう。
でも自分の携帯電話は905シリーズなので、例えそのようなサービスが提供されても使えないので、前回作ったPicasa Ploxy Server を中継して写真をアップロードするiアプリを作成した。

以下はそのソースコード。



import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import javax.microedition.io.Connector;

import com.nttdocomo.io.ConnectionException;
import com.nttdocomo.io.HttpConnection;
import com.nttdocomo.system.ImageStore;
import com.nttdocomo.system.InterruptedOperationException;
import com.nttdocomo.system.StoreException;
import com.nttdocomo.ui.Canvas;
import com.nttdocomo.ui.Dialog;
import com.nttdocomo.ui.Display;
import com.nttdocomo.ui.Frame;
import com.nttdocomo.ui.Graphics;
import com.nttdocomo.ui.IApplication;
import com.nttdocomo.ui.Image;
import com.nttdocomo.ui.MediaImage;


public class iPicasa extends IApplication {
public void start() {
iPicasaCanvas canvas = new iPicasaCanvas();
Display.setCurrent(canvas);
}

class iPicasaCanvas extends Canvas {
String proxyUrl = null;
int id = -1;
MediaImage media = null;
Image image = null;

public iPicasaCanvas(){
proxyUrl = getArgs()[0];
setSoftLabel(Frame.SOFT_KEY_1,"終了");
setSoftLabel(Frame.SOFT_KEY_2,"選択");
}

private byte [] post(String url, byte [] data) throws IOException {
HttpConnection conn = null;
OutputStream os = null;
InputStream is = null;
try {
conn = (HttpConnection)(Connector.open(url, Connector.READ_WRITE, true));
conn.setRequestMethod(HttpConnection.POST);
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

os = conn.openOutputStream();
os.write(data);
os.close();

conn.connect();

is = conn.openInputStream();

ByteArrayOutputStream response = new ByteArrayOutputStream();
byte buff[] = new byte[1024];
int len;
while((len = is.read(buff)) != -1) {
response.write(buff, 0, len);
}
is.close();

return response.toByteArray();

} finally {
try{
if (os != null) os.close();
if (is != null) is.close();
if (conn != null) conn.close();
} catch (IOException ioe) {}
}
}

private void selectImage() throws InterruptedOperationException, ConnectionException {
ImageStore imageStore = ImageStore.selectEntry();
if (imageStore == null) {
return;
}
id = imageStore.getId();
media = imageStore.getImage();
media.use();
image = media.getImage();

setSoftLabel(Frame.SOFT_KEY_2,"登録");

repaint();
}

private void cretaePhoto() throws IOException, StoreException {
InputStream in = ImageStore.getEntry(id).getInputStream();
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte buff[] = new byte[1024];
int len;
while((len = in.read(buff)) != -1) {
out.write(buff, 0, len);
}
in.close();

String response = new String(post(proxyUrl+"/createPhoto.do", out.toByteArray()));
if (!"OK".equals(response)) {
Dialog dlg = new Dialog(Dialog.BUTTON_OK, "画像登録");
dlg.setText("失敗しました");
dlg.show();
return;
}

Dialog dlg = new Dialog(Dialog.BUTTON_OK, "画像登録");
dlg.setText("登録しました");
dlg.show();

id = -1;
media.unuse();
media.dispose();
image = null;

setSoftLabel(Frame.SOFT_KEY_2,"選択");

repaint();
}

public void processEvent(int type, int param) {
try {
if (type == Display.KEY_RELEASED_EVENT) {
switch(param) {
case Display.KEY_SOFT1:
terminate();
break;
case Display.KEY_SOFT2:
if (id == -1) {
selectImage();
} else {
cretaePhoto();
}
break;
}
}
} catch (Exception e) {
Dialog dlg = new Dialog(Dialog.BUTTON_OK, "error");
dlg.setText(e.toString());
dlg.show();

e.printStackTrace();
}
}

public void paint(Graphics g) {
g.lock();
g.setColor(Graphics.getColorOfRGB(0xFF, 0xFF, 0xFF));
g.fillRect(0, 0, Display.getWidth(), Display.getHeight());
if (image != null) {
float scaleX = (float) Display.getWidth() / (float)image.getWidth();
float scaleY = (float) Display.getHeight() / (float)image.getHeight();

float scale = (scaleX < scaleY) ? scaleX : scaleY;
if (scale > 1.0f) {
scale = 1.0f;
}

int width = (int)(image.getWidth() * scale);
int height = (int)(image.getHeight() * scale);
int dx = (Display.getWidth()-width)/2;
int dy = (Display.getHeight()-height)/2;

g.drawScaledImage(image, dx, dy, width, height, 0, 0, image.getWidth(), image.getHeight());
}
g.unlock(true);
}
}
}


iアプリの通信機能はグローバルなドメインが必要なので、このiアプリの利用には、残念ながらドメインを持っている人でなければ使えない。
なので私は無料のダイナミックDNSを使ってドメインを取得、試験してみた。

で、問題発見。

iアプリのPOSTは1回の通信あたり80Kバイトまでに制限されていたことを忘れており、あまり大きな画像がアップロードできない。
今の携帯携帯カメラはメガピクセル。
これは利用に当たって問題なので、いずれ修正したい。

2008-07-04

最初の一筆。

昔から日記は続いたことはないし、人に文章を見てもらうことにもなれていない。
でもメモはよくとってきたので、肩肘はらず、メモを残すつもりではじめたいと思う。

まずは旅行にいくので、googleを旅行先で活用できないか調べたい。

使ってみたいのは、Picasa Web Albums。
携帯電話からも Picasa は使えるけど、あくまで閲覧だけの様子。

旅行先で写真を撮って、簡単に写真と場所を保存できたらいいなあ。
で調べたのは Picasa Web Albums Data API

google Data API は使ったことがないので、よい勉強になるだろう。
ざっと見てみると google Data APIは、 AtomRSSAtom Publishing Protocol (APP) といったフォーマット、RESTのアプローチでサービスを提供しているよう。

動くものを作ってみたくなったので、Javaによる実装を挑戦。
google のサンプルソースコードは読みやすく、すぐ応用できた。

まずは簡単に写真を登録する Proxy Server を作ってみた。
以下はそのソースコード。


import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Date;
import java.util.List;
import java.util.ResourceBundle;

import org.apache.log4j.Logger;

import com.google.gdata.client.photos.PicasawebService;
import com.google.gdata.data.OtherContent;
import com.google.gdata.data.photos.AlbumEntry;
import com.google.gdata.data.photos.PhotoEntry;
import com.google.gdata.util.ContentType;
import com.google.gdata.util.ServiceException;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

public class PicasaProxy extends PicasawebClient {
private static final Logger logger = Logger.getLogger(PicasaProxy.class);

final HttpServer server;

AlbumEntry albumEntry;

public PicasaProxy(PicasawebService service, String uname, String passwd,
int alubum, String address, int port)
throws IOException, ServiceException {

super(service, uname, passwd);

logger.debug("uname:"+uname);
logger.debug("passwd:"+passwd);
logger.debug("alubum:"+alubum);
logger.debug("address:"+address);
logger.debug("port:"+port);

selectAlubum(alubum);

String[] parts = address.split("\\.");
byte [] addr = new byte[parts.length];
for (int i=0; i < parts.length; i++) {
addr[i] = (byte)Integer.parseInt(parts[i]);
}
InetAddress inetAddress = InetAddress.getByAddress(addr);
InetSocketAddress hostPort = new InetSocketAddress(inetAddress, port);

server = HttpServer.create(hostPort, 1);
server.createContext("/createPhoto.do", new CreatePhoto());
}

public void start() {
server.start();
}

private void selectAlubum(int index) throws IOException, ServiceException {
List albums = getAlbums();
albumEntry = albums.get(index);
System.out.println(albumEntry.getId());
}

class CreatePhoto implements HttpHandler {
public void handle(HttpExchange httpExchange) throws IOException {
logger.debug("CreatePhoto Start");
try {
InputStream in = httpExchange.getRequestBody();
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte buff[] = new byte[1024];
int len;
while((len = in.read(buff)) != -1) {
out.write(buff, 0, len);
}
in.close();

PhotoEntry photoEntry = new PhotoEntry();
photoEntry.setTitle(albumEntry.getTitle());
photoEntry.setTimestamp(new Date());
OtherContent content = new OtherContent();
content.setBytes(out.toByteArray());
content.setMimeType(new ContentType("image/jpeg"));
photoEntry.setContent(content);

insert(albumEntry, photoEntry);

photoEntry = null;

final String response = "OK";
httpExchange.sendResponseHeaders(200, response.getBytes().length);
httpExchange.getResponseBody().write(response.getBytes());
httpExchange.close();
} catch (Exception e) {
logger.equals(e);
final String responseError = "ERROR: 500 Internal Server Error";
httpExchange.sendResponseHeaders(500, responseError.getBytes().length);
httpExchange.getResponseBody().write(responseError.getBytes());
httpExchange.close();
}
logger.debug("CreatePhoto End");
}
}

public static void main(String[] args) throws Exception {
PicasawebService service = new PicasawebService("PicasaProxy");

ResourceBundle rb = ResourceBundle.getBundle(PicasaProxy.class.getName());
String uname = rb.getString("uname");
String passwd = rb.getString("passwd");
int alubum = Integer.parseInt(rb.getString("alubum"));
String address = rb.getString("address");
int port = Integer.parseInt(rb.getString("port"));

PicasaProxy proxy =
new PicasaProxy(service, uname, passwd, alubum, address, port);
proxy.start();
}
}

今度はiアプリを使り、連携して写真を登録できるようにしよう。