Offline Android Map Sample App using MBTiles + Leaflet

仕事で検討する機会があったので、TileMillで作ったMBTilesとLeafletでオフラインのAndroid(なんちゃって)地図アプリのサンプルを作ってみました。
最初にはJavaScriptからMBTilesにアクセスしようとしたのですが、何故かdatabasePathの設定が効かずに/data/data/package/webveiw.dbをみにいってしまうので、db周りはAndroid側でやっています。
DBヘルパーはjgilfelt/android-sqlite-asset-helper · GitHubを使わせて頂きました。
特に独自の技術ではなく、オープンソースをつなぎ合わせただけですが、割と需要があるかと思いますので、シェアします。
monomoti/OfflineLeaflet · GitHub

MapBoxいいですね。

Google Maps APIで動くタイル地図をやってみました。

id:kochizufanさんのアイディアを得て、タイルが動く地図を作ってみました。

4フレームのアニメーションとする事にして、まずはこういう256 × 1024のタイルを作りました。

次は、Google Maps APIのMapTypeの定義です。

var monomoti = {};

monomoti.AnimationMapType = function (tileDir,ext,maxZoom,frameLength) {
	this.maxZoom = maxZoom;
	this.tileDir = tileDir;
	this.ext = ext;
	this.frameLength = frameLength;
	this.tileSize = new google.maps.Size(256,256);
};

monomoti.AnimationMapType.prototype.getTile = function(coord, zoom, ownerDocument) {
	var y = (1 << zoom) - 1 - coord.y;
	imgUrl = this.tileDir + "/"  + zoom + '/' + coord.x + '/' + y + this.ext;
	return this.createBackGroundTile(ownerDocument,imgUrl);		
};

monomoti.AnimationMapType.prototype.createBackGroundTile = function(ownerDocument,url){
	var div = ownerDocument.createElement('DIV');
	div.setAttribute("class","animationTile");
	div.style.width = this.tileSize.width + 'px';
	div.style.height = this.tileSize.height + 'px';
	if (url){
		div.style.backgroundImage = "url('" + url + "')";
		div.style.backgroundRepeat = "no-repeat";
	}else{
		div.style.backgroundColor = this.backgroundColor;
	}
	return div;
};
monomoti.AnimationMapType.prototype.name = "animationmap";
monomoti.AnimationMapType.prototype.alt  = "animationmap";

利用例です。

<!DOCTYPE html>
<html> 
<head> 
<meta content="yes" name="apple-mobile-web-app-capable" /> 
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" /> 
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
<meta name="description" content="animated tile map">
<title>Animated Tile Map</title> 
<style type="text/css" media="screen">
	body{
		padding: 0;
		margin: 0;
	}
</style>
<script src="http://maps.google.com/maps/api/js?sensor=false&v=3" type="text/javascript"></script>
<script src="./javascript/jquery-1.8.3.min.js" type="text/javascript" charset="utf-8"></script>
<script src="./javascript/implementation.js" type="text/javascript" charset="utf-8"></script>


<script type="text/javascript"> 
var map;
var mapContainer;
var isPlaying = false;
var aTimer;
var initialFrame = 1;
var frameLength = 4 ;
var currentFrame = initialFrame;
$(function(){
	set100Pct();
	$(window).resize(function(){
		set100Pct();
	});	
	mapContainer = document.getElementById("map");
	
	map = new google.maps.Map(mapContainer,{mapTypeControl: false});
	var animationMapType = new monomoti.AnimationMapType("./movingtiles",".png",18,frameLength);
	map.mapTypes.set('animationtile',animationMapType);
	map.setMapTypeId('animationtile');

	map.setOptions({streetViewControl:false,minZoom:13,maxZoom:18});
	map.setCenter(new google.maps.LatLng(34.743446,135.765953));
	map.setZoom(16);	

	toggleAnimation();
	
});

function set100Pct(){
	var dw = $(window).width();
	var dh = $(window).height();
	$("#main").css({width:dw + "px",height:dh + "px"});	
	
}
function toggleAnimation(){
	if (isPlaying){
		if (aTimer){
			clearInterval(aTimer);
		}
		aTimer = null;
		
		$("#control a").text("PLAY");
		isPlaying = false;
	}else{
		aTimer =setInterval(function() {
			currentFrame = ((currentFrame) % frameLength) + 1;
			yOffset = 256 * (currentFrame - 1);
			$(".animationTile").css({"background-position":"0px -" + yOffset + "px"});
		}, 1000);

		$("#control a").text("STOP");
		isPlaying = true;
	}
}

</script> 
</head> 
<body>
	<div id="main" style="position:absolute;">
		<div id="map" style="width:100%; height:100%"></div>
	</div>
	<div id="control" style="position:absolute;top;20px;left:70px;background-color:#fff">&nbsp;<a href="#" onClick="toggleAnimation();"></a>&nbsp;</div>
</body> 
</html> 

需要があるか分かりませんが、ここでデモが見られます。
http://monomoti.daa.jp/movingmap/

TileMill+UTFGrid+Leafletで地球を物色する

【音量注意】

貴方は宇宙人です。貴方の仕事は地球を侵略する事です。
さあ、この地図(http://monomoti.daa.jp/invadermap)で最も効果的な攻撃目標を探すのです。
"SEARCH"をクリックしてパン&ズームすれば、地図が音で目標を知らせてくれるので、新人の貴方でも大丈夫。
あ、いうまでもない事ですが、意識の高い宇宙人はIEOPERAは使わないのですよ。FireFoxSafariChromeしか認めません。いいですね?

中国の鳴りが良い

しょうもないものを作ってすみません...が一応ご説明します。
TileMillの白地図レイヤ(country)で生成されるUTFGrid*1をLeafletで参照して、地図の中心緯経度のPOP_EST属性の値を基に音の高低差を付けています。
UTFGridを使うなら本当はmapbox.jsでやるのがいいと思ったのですが、ヘタレな僕は任意の座標のグリッドの取得方法で若干ハマりそうだったので、LeafletのUTFGridプラグインhttps://github.com/danzel/Leaflet.utfgrid)を利用して、下記の様にしてみました。

	var map = utfGrid._map,
	    point = map.project(latlng),
	    tileSize = utfGrid.options.tileSize,
	    resolution = utfGrid.options.resolution,
	    x = Math.floor(point.x / tileSize),
	    y = Math.floor(point.y / tileSize),
	    gridX = Math.floor((point.x - (x * tileSize)) / resolution),
	    gridY = Math.floor((point.y - (y * tileSize)) / resolution),
		max = map.options.crs.scale(map.getZoom()) / tileSize;

	x = (x + max) % max;
	y = (y + max) % max;

	var data = utfGrid._cache[map.getZoom() + '_' + x + '_' + y];
	if (!data) {
		return { latlng: latlng, data: null };
	}

	var idx = utfGrid._utfDecode(data.grid[gridY].charCodeAt(gridX)),
	    key = data.keys[idx],
	    result = data.data[key];

来年の抱負

来年は、勤め先ではFOSS4Gツールを導入して作業を効率化し、個人的にはid: kochizufanさんの歴史国土や地図タイル工法協会で勉強させてもらって、ジオ充を目指します。

明日は、Noといえる男@Say_noさんです!

当エントリは、 FOSS4G Advent Calendar 2012 の12/19分として投稿したものです。ぜひ他の方のエントリも御覧下さい。

*1:本当は、id:tmizu23さんのラブリーなツールtile2utfgridを使って、標高で音を鳴らす「地図オルゴール」にしたかったのですが、OFF4Gのあとなかなか酒が抜けず、時間切れになりまして...orz

FOSS4G2012 Tokyo/Osakaが開催されます!

11/3〜5に東京、11/7,8に大阪で、フリー&オープンなGISの祭典、FOSS4Gが開催されます。

GISとはGeographic Information Systemの略で「地理的位置を手がかりに、位置に関する情報を持ったデータ(空間データ)を総合的に管理・加工し、視覚的に表示し、高度な分析や迅速な判断を可能にする技術」(「GISとは・・・|国土地理院」より)のことです。
最近iOS6で話題になった、デジタル地図のデータを作成したり、インターネットで配信する技術もGISに含まれます。
そのGISのフリー&オープンソースのソフトウェア/ツールがFOSS4G(Free &Open Source Softoware for Geospatial)なのです。
FOSS4Gって、デジタル地図以外にも、さまざまな目的で多くの企業や団体、研究機関で使われているんですよ。
このイベント(これもFOSS4Gと呼ばれています)では、ソフトウェア/ツールの最新動向や利用事例の発表が聞け、またまた初心者にもわかりやすいハンズオンセッションもあります。詳細はOSGeo.jp公式サイト(http://www.osgeo.jp/%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88/foss4g2012tokyohttp://www.osgeo.jp/%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88/foss4g2012osaka/)をご覧ください。
皆さん、ぜひご参加ください!


おまけ。
数年前から、毎年このイベントの「プロモーション音楽」を作っています。今年も誰に頼まれたわけでもなく作りました。

Download

「FOSS4Gの歌」 By monomoti

凄いこと
できる人
こないだ
出会った

長い間
困っていた
事を
片付けた

なんやかんや
使ってた
面白そうで
ねほり、はほり

「そういうツール
彼はいう
「いろいろ
あるよ」と

目的と
手段と
材料の
バランス

聞いた事は
全部わかった
わけではないけれど
この名前は覚えてる

ああ、それは
FOSS4G
自由な道具
ああ、それは
FOSS4G
誰でも手に入れられる
FOSS4G

彼には
楽しみに
している
日がある

毎年
西と東
彼みたいな人が
集まる

会ったことは
ないけれど
面白そうだ
その話が
出来るなら

ああ、それは
FOSS4G
新しい出会い
ああ、それは
FOSS4G
そこに行けば高めあえる
そこがFOSS4G

高めあえる
支えあえる
分かりあえる

ああ、それは
FOSS4G
明らかな自由

ああ、それは
FOSS4G
新しい出会い

ああ、それは
FOSS4G
しなやかな心

そう、それが
それがFOSS4G
誰でも君でも始められる
それがFOSS4G

leafletはじめました

当エントリは、 FOSS4G Advent Calendar 2011 の12/19分として投稿したものです。ぜひ他の方のエントリも御覧下さい。
(錚々たる面々の中に割り込んでしまってビクビクしておりますが、くじけず最後まで頑張ります!)

こんにちは。monomotiです。「ちずぶらり」という古地図/絵地図アプリのデータのオーサリングやサーバ・web周りの開発をやっています。
その中でしばらくleafletを触る機会がありましたので、いろいろレポートしたいと思います。

leafletとは

釈迦に説法とは思いますが、一応このleafletとは何か説明しておきますと、JavaScriptで書かれたタイルベースの地図クライアント・ライブラリです。
その特徴は、

軽量(フル装備で69KB)!
動作がヌルヌルしていてお洒落!

というものです。ヌルヌル感の例をあげると、ズームは離散ズームなのですが、ズームチェンジ時にちょっとしたアニメーションが入っていてガタ付きを感じさせないようになっています。また、ピンチイン等でズームレベル0から1に向かってズームインするとき、途中で手を止めてもズームレベル1にヌルッとズームインしてくれます。こういう動作はユーザには結構大事みたいで、同じ機能の物を作って見せてもleafletとそれ以外では反応がかなり違います。これだけでも、僕にとってはlealfletを使いたくなる十分な理由になります。

はじめの一歩

では早速、使い方を。まずはファイルの設置。
http://leaflet.cloudmade.com/download.htmlからパッケージをDLして解凍します。解凍されたフォルダの中にdistというフォルダがあるので、この中身を任意のフォルダにコピーします。

あとは、HTMLのhead要素に次のようにリンクを入れれば準備完了です。

<head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8"/>         
    <title>Leaflet Cho Kantan</title>
    <link rel="stylesheet" href="leaflet/leaflet.css" />
    <script src="leaflet/leaflet.js"></script>
</head>

なお、jsファイルはbuild/build.html で必要なコンポーネントだけを選択してビルドすることで、より小さなサイズにすることができます。

では、地図を表示させてみましょう。HTMLに次のdiv要素があるとして、

<body>
    <div id="map"></div> <!-- これを地図のコンテナ要素にする -->
</body>

下のようなJavaScriptを書きます。

// ベースタイルのレイヤを定義
// {z}、{x}、{y}はそれぞれズームレベル、タイルの水平方向インデックス、タイルの垂直方向インデックスのプレースホルダ。
var tileUrl = 'タイルのURL{z}_{x}_{y}.jpg', 
    tileAttribution = 'タイルの権利帰属等',
    baseLayer = new L.TileLayer(tileUrl, {maxZoom: 18, attribution: tileAttribution });

// マップオプジェクト。引数はコンテナ要素のIDまたはコンテナ要素
var map = new L.Map('map'); 

// マップにベースタイルを追加する
map.setView(new L.LatLng(34.637728,135.40889713).addLayer(baseLayer);

これで地図が表示されます。

いろいろ弄れる

例えば投影はEPSG:3857、4326、3395が予め定義されていますが、独自の投影を定義する事も出来ます。

// 投影座標系を定義
var  myProj0001 = L.Util.extend({}, L.CRS, {
     code: 'MYPROJ:0001',

     projection: {
          project: function(latlng) {
               //経緯度latlngを平面座標に変換するコードをここに書く
               
               return new L.Point(平面座標X, 平面座標Y);
          },
          unproject: function(point, unbounded) {
               // 平面座標pointを経緯度に変換するコードをここに書く

               return new L.LatLng(緯度, 経度);
          }
     },
     transformation: new L.Transformation(1,0,1,0)    
});

// 投影座標系を指定してマップオブジェクトを生成。     
var map = new L.Map('map',{crs:myProj0001});

これで手書きの馬鹿地図なんかも表示出来るようになります。オープンソースって素敵ですね。

面倒くさがりな貴方に(いや僕に)うれしいAPI

APIGoogle Map APIにかなり似ていて、Google Mapsを触った事がある人なら特にドキュメントを調べる必要もなくコーディング出来ます。さらに、leafletにはlatLngToLayerPointやcontainerPointToLayerPointといった気の利いた座標変換メソッドがあって、これが結構便利です。
例えば僕は、センターマーカの表示なんかに使ったりしています。

var cmk;
function setCenterMaker(){
// (jQuery使ってます)
    if (!cmk){
        cmk = $(document.createElement("img"));
        cmk.attr("src", "./images/cmk.gif").css({position:"absolute",zIndex:999});
        $("#map").append(cmk);
    }
    var cll = lLMap.getCenter();
    var cxy = lLMap.layerPointToContainerPoint(map.latLngToLayerPoint(cll));
    cmk.css({
        top: cxy.y - 20 + "px",
        left: cxy.x - 20 + "px"
    });
}
$(window).resize(setCenterMaker);


「コンテナの幅と高さを取って真ん中に置けばいいんじゃないの?」と言われそうですが、自分でサイズを取得するとよく間違えるので、僕はコンテナ要素を極力気にしたくないんです...。上記のコードだと、コンテナを意識するのはセンターマーカを生成する時だけになるので、この方が楽な気がします(ちょっと遅くなっていると思いますが)。

気をつけるべき点

最後に、留意すべき点を1つあげておきます。それは、他のHTML要素に対する地図コンポーネントの振る舞いが、ブラウザによって異なるという事です。
例えば、マーカをクリックしてマーカの属性をポップアップで表示する、というのはよくやる事だと思います。leafletにはGoogle Maps APIのinfowindowと同じようなpopUpというクラスがあり非常に使いやすいのですが、

実際の業務では自前のHTML要素をオーバーレイさせる事の方が多いと思います。

僕も下のような感じでやってみました。

<div id="map" style="width:100%; height:100%"></div>
<div id="popUp" >
	<p id="popUpContent" ></p>
	<div class="closeButton">&nbsp;x&nbsp;</div>
</div>
var mk1 = new L.Marker(new L.LatLng(34.637728,135.40889713),{message:"Hello!"});
mk1.on("click",function(e){
	showPopUp(e.target.options.message);
});
map.addLayer(mk1);

$(".closeButton").click(function(){
	$(this).parent().hide();
});

function showPopUp(content){
	$("#popUpContent").html(content);
	var dWidth = $(window).width() / 2,
	dHeight = $(window).height() /2,
	pH = dHeight / 2,
	pW = dWidth / 2;
	$("#popUp").show().css("top",pH + "px").css("left",pW + "px").height(dHeight).width(dWidth);
}

が、FireFoxだと何も表示されず、悩むこと十数分(悩み過ぎ)。エラーログには何もないし、何でだろう?と思いながらベースレイヤを切り替えてみたら...

そこに居たのか...。どうやら、FireFoxではレイヤオブジェクトは後に出現するHTML要素よりも前に出てきてしまうようです。こういう事が他にもあるのかないのか。
あと、asusタブレットではタイルの配置がおかしくなるという不具合があります(Eee Pad Transformerで確認)。

まとめ

leafletは、ルック&フィールやAPIが非常に洗練されていてとても使いやすいです。お客さんにも受けが良いです。しかし、クロスブラウザ環境での動作についてはまだまだ問題がありそうです。ひととおり挙動が分かってしまえば大した事はないですが、癖が分かっていない今の状況では、ちょっと怖くて僕はまだ実戦(製品)で使えていません。しかしできるだけ早く実戦で使ってみたいので、これからどんどん試作をして検証していきます。また、時間があればプロジェクトにも貢献したいなと思います。

レポートは以上です! 次はnissyyuさんです!

(こんなエントリで良かったんでしょうか。FOSS4Gソングの作り方の方がよかったかな...)

声枯れつづけて30年

ブライアン・アダムス、来るんやー。2/13大阪城ホールか。観たい。
マセて「洋楽しか聴きとうない!」と突っ張ってた小学生だった自分が、最初に聴いたハードなロックンロールが"Kids Wanna Rock"だった。それで人生が変わったとは言えんけど、衝撃やった。
そういう人を、やっぱ一度は生で観とかないと。行く行く。観に行く。