こんにちは。LatLongLabの河合(@inuro)です。
先日、ルートラボというサービスをリリースしました。主として自転車乗りの方などにご愛用いただいていたALPSLAB routeというサービスの後継となります。今年3月末のALPSLABのクローズにともない、LatLongLabに移行しました。
もちろんただ移行するだけでは相対的には退化なので、ルートラボでも色々と新しい試みを行っています。
そもそもルートラボとは
ルートラボは、サイクリングコースや道案内などのルートを簡単に作成し公開できるサービスです。
- 地図をクリックするだけのシンプル操作でルートが描ける
- 地図と標高グラフが併せて表示される
- 作成したルートをWebページに貼り付けたりTwitterでシェアできる
ことが主な特徴です。
iPhoneへの対応
今回、ルートラボはiPhoneからの閲覧に対応しました。クロスブラウザ対策を考えなくてはならないPC向けビューと異なり、iPhone向けビューはSafariを前提とした、より突っ込んだ実装が可能です。
ルートラボで利用しているHTML5とCSS3の機能をいくつかご紹介します。
GeoLocationサービスで現在位置の表示
iPhoneでは navigator.geolocation オブジェクトを通じて現在位置の情報をJavascriptから利用することができます。
ルートラボでは、現在地の周辺ルートの検索と、ルート閲覧中に地図上への現在位置のプロットに利用しています。
具体的には、ルート閲覧では navigator.geolocation.watchPositionを用いて測位を常時監視し、現在位置を更新しています。
navigator.geolocation.watchPosition(function(position){
var lat = position.coords.latitude;
var lng = position.coords.longitude;
var acc = parseInt(position.coords.accuracy, 10);
// 現在位置を表す円の位置と大きさを更新
// ...
});
latitude 、 longitude で、現在位置の緯度経度が世界測地系で度単位で得られます。
得られた緯度経度の値は、YOLPのJavascript地図APIで、Y.LatLngコンストラクタにそのまま渡すことができます。
var map = new Y.Map("mapdiv");
var pos = new Y.LatLng(lat, lng);
map.panTo(pos);
accuracyで得られる測位精度の値は、存在可能範囲の半径(単位はメートル)になっていますので、これをparseIntし縮尺に合わせてメートル→ピクセルに変換して、現在位置を表す円の半径として使っています。
CSS3で角丸グラデーションボタン
iPhoneのSafariはCSS3をサポートしているので、これまでは静的な画像でなければ難しかったリッチなグラフィックスボタンもクライアント側で動的に描画することができます。
例えばこの「検索」ボタンはこのようなCSSで実装されています。
<style type="text/css">
button.push{
padding:0.2em;
border: solid 1px #c99;
background-color: #c99;
background-image: -webkit-gradient(
linear,
0% 0%,
0% 100%,
from(rgba(255, 255, 255, 0.7)),
to(rgba(155, 155, 155, 0.1)),
color-stop(.5,rgba(255, 255, 255, 0.1)),
color-stop(.5,rgba(155, 155, 155, 0.2))
);
-webkit-border-radius: 8px;
-webkit-box-shadow: rgba(0,0,0,.2) 1px 2px 4px;
color: rgba(255,255,255,1.0);
font-size:14pt;
font-weight:bold;
text-shadow: rgba(0,0,0,0.3) 0px -1px 1px;
}
</style>
<button type="submit" class="push"><span>検索</span></button>
内容としては
- -webkit-border-radiusでボックスの角を丸める
- -webkit-box-shadowでボックスに影をつける
- background-imageに-webkit-gradientで「iPhoneぽい」グラデーションを設定する
- text-shadowでテキストに影をつけてエンボスぽくする
という具合です。background-colorの値を変えることでボタンの色を変えることができます。「もうちょっと赤味が強い方がいい」「やっぱページ中のボタン全部青くして」といった無理難題に迅速に対応することも容易ですね。
<canvas>でアイコンの動的描画
また、この「>」アイコンは<canvas>で描画しています。もちろんこの程度の画像であれば静的に用意してもよいのですが、<canvas>で動的に描くことでサイズや色の変更、回転といったフロントエンドの制作過程における試行錯誤が容易になります。
具体的には以下のようなコードでドキュメントに動的に挿入しています。
function appendArrow(container){
//create canvas
var canvas = document.createElement('canvas');
//dom object size
canvas.style.width = '30px';
canvas.style.height = '30px';
//canvas viewport size
canvas.width = '30';
canvas.height = '30';
//path(6x6)
var path = [
{x:1, y:0}
, {x:4, y:3}
, {x:1, y:6}
, {x:0, y:5}
, {x:2, y:3}
, {x:0, y:1}
];
//multiply (30x30)
for(var i = 0; i < path.length; i++){
path[i].x *= 5;
path[i].y *= 5;
}
//draw
var dc = canvas.getContext('2d');
dc.beginPath();
dc.moveTo(path[0].x, path[0].y);
for (var i = 1; i < path.length; i ++) {
dc.lineTo(path[i].x, path[i].y);
}
dc.closePath();
dc.fill();
//append
container.appendChild(canvas);
}
appendArrow(document.getElementById('container'));
上記はだいぶ冗長なコードになっています。
<canvas>の要点は
- <canvas>のCSSによるwidth/heightは、DOMオブジェクトのサイズを定義する
- <canvas>のアトリビュートとしてのwidth/heightは、実キャンバスのビューポートを定義する
- 実キャンバスに描かれた ラスタ画像がビューポートでクリップされ、DOMオブジェクトのサイズ一杯にストレッチされて表示される
といったあたりです。
TV放送に例えればCSSによるstyle.width/style.heightがTVの大きさ、アトリビュートとしてのwidth /heightが映像コンテンツのサイズです。4x3の映像を16x9のTVにそのまま映せば横長に引き伸ばされた絵になりますし、SD解像度の映像をフルHDTVに映せば拡大されてボヤけた感じになるのと同じです。
上記コードの例で言えば、30x30に描画した「>」アイコンを30x30pxの範囲にストレッチしています。といってもこの場合dot by dotであり拡縮は発生していません。意図通りの描画になっています。
これを、例えばこのようにstyle.heightを半分にすると
canvas.style.width = '30px';
canvas.style.height = '15px';
このように縦に潰れて表示されます。
ではこのようにstyle.width/height共に2倍にしたらどうなるでしょうか。
canvas.style.width = '60px';
canvas.style.height = '60px';
なんとなく綺麗に2倍に拡大された「>」アイコンを想定してしまいますが、実際にはこうなります。
つまり<canvas>はベクタ描画ではなくあくまでラスタ描画なので、30x30pxで一旦レンダリングした画像が60x60px に引き伸ばされるためこうなってしまいます。
HTML5のワーキングドラフトでもこう述べられています。
The canvas element provides scripts with a resolution-dependent bitmap canvas
ベクタ描画には<svg>、ラスタ描画には<canvas>と使い分けましょう。
localStorageで検索履歴の保存
HTML5にはWeb Storageと呼ばれるクライアント側でデータを蓄積・永続化する仕組みがあります(Webと名が付いてますがサーバ側ではありません)。
ルートラボではその1つであるlocalStorageを利用して、検索履歴の保存を行っています。
Web Storageはシンプルなkey-valueストアです。インタフェースとしてはkeyを指定したsetItem、 getItem、removeItem、それとlengthおよびインデックス指定によるkeyの取得(つまりforループ用)、あとデータ全体のclear、とこれだけです。
ルートラボではkeyを検索語、valueを検索回数として検索履歴を保存しています。
var storeKeyword = function(keyword){
var storage = window.localStorage;
if(storage){
var count = 0;
if((count = storage.getItem(keyword)) > 0){
count++;
storage.removeItem(keyword);
storage.setItem(keyword, count);
}
else{
storage.setItem(keyword, 1);
}
}
}
実際にはremoveItemは行わなくても、setItemに既存のkeyを指定すれば上書きしてくれます。
保存した検索語は、このようにして表示しています。
var showKeywords = function(){
var storage = window.localStorage;
if(storage){
for(var i=0; i<storage.length; i++){ //左のstorageは全角です、半角に直してご利用ください。
var keyword = storage.key(i);
var count = storage.getItem(key);
//print "keyword(count)" ...
}
}
}
localStorageは、COOKIEと異なり基本的には無期限に保存されます。
localStorageの設定インタフェースを提供していないブラウザも多く、現にiPhoneのSafariにはありません。
古いデータを削除するなどのメンテナンス機能はサイト提供者側で明示的に用意する必要があります。
ルートラボでは保存された検索キーワードを一括で削除する機能を用意しています。
var clearKeywords = function(){
var storage = window.localStorage;
if(storage){
if(confirm('過去の検索キーワードをクリアしてよろしいですか?')){
storage.clear();
}
}
});
なおlocalStorageはオリジン単位でユニークです。つまりドメインとポート番号が一致するドキュメントからは全て同じlocalStorageが参照できますし、できてしまいます。ページ間をまたいでデータを利用するのには便利ですが、同じドメインで複数のアプリケーションを運用する場合などは切り分けに注意する必要があります。
おわりに
Javascriptでプログラムロジックが組め、Web Storageでローカルストレージが使え、CSS3や<canvas>で動的・構造的なグラフィックスのコントロールが可能なiPhone のSafariはアプリケーションプラットフォームとして充分な能力を備えています。
iPhoneアプリが百花繚乱ですが、HTML5でiPhoneアプリケーションというのも面白いのではないでしょうか。なにせインストールも AppStore申請もいりません。フィードバックを頂いて再リリースするサイクルが短いというのは、アプリケーションの品質を高める上でなにより強力なアドバンテージとなります。
ルートラボはじめLatLongLabでもさらなるiPhoneならではの体験を引き続き提供していきたいと思います。みたいな。
こちらの記事のご感想を聞かせください。
- 学びがある
- わかりやすい
- 新しい視点
ご感想ありがとうございました