まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

タグ:map

このエントリの続きです:
Leaflet.js の UI で画像を表示する(1)


地図操作ライブラリの Leaflet.js の応用で、地図以外のオブジェクトを Leaflet の UI で表示する方法を紹介しています。前回は imageOverlay を使って画像を表示しましたが、今回は canvasOverlay を使ってキャンバス(HTML5 の <canvas>)を操作する方法を紹介します。比較の意味も含めて、表示内容そのものは前回同様に画像としますが、画像を画像のまま表示するのではなく、画像をキャンバスに描画した上で、更にキャンバスに描画を加える、という内容を紹介します。必要に応じて前回の内容も参照しながら見てください。

なお今回も表示対象画像は「いらすとや」「家にいるアマビエ」の画像とします:
amabie_stay_home


また前回紹介した内容は Leaflet.js の標準機能だけで実現しましたが、今回はアドイン機能である L.CanvasLayer を併用して実現します。以下のサイトからあらかじめ L.CanvasLayer.js をダウンロードしておいてください:
https://raw.githubusercontent.com/Sumbera/gLayers.Leaflet/master/L.CanvasLayer.js


ダウンロードできたら以下のような HTML を用意します:
<script src="./L.CanvasLayer.js"></script>

<div id="demoMap"></div>

<script>
//. 空の地図を OpenStreetMap データで表示
map = L.map('demoMap', { dragging: true, zoomControl: true, minZoom: -5, maxZoom: 5, crs: L.CRS.Simple, preferCanvas: true } );

//. 表示する画像のパスとサイズ
var image = {
  url: './3600x3600.png',
  width: 3600,
  height: 3600
};

//. 画像領域を地図に設定
var imageBounds = L.latLngBounds([
  map.unproject( [ 0, image.height ] ),
  map.unproject( [ image.width, 0 ] )
]);
map.fitBounds( imageBounds );
map.setMaxBounds( imageBounds.pad( 0.5 ) );

//. 画像をオーバーレイで設定
L.imageOverlay( image.url, imageBounds ).addTo( map );

//. キャンバスレイヤーを設定
L.canvasLayer().delegate( this ).addTo( map );
      
//. キャンバスの再描画イベントハンドラ
function onDrawLayer( info ) {
  var canvas = info.canvas;
  var ctx = canvas.getContext( '2d' );

  var colors = [
    "rgba(0,0,0,0.2)",
    "rgba(255,0,0,0.2)"
  ];
  var points = [
    [ -1000, 2000 ],
    [ -2000, 2250 ]
  ];

  ctx.clearRect( 0, 0, canvas.width, canvas.height );

  //. ズームレベルに合わせて arc の描画半径を調整
  var zoom = map.getZoom();
  var r = 30 * Math.pow( 2, zoom );

  for( var i = 0; i < points.length; i++ ){
    var point = points[i];
    if( info.bounds.contains( [ point[0], point[1] ] ) ){
      var p = info.layer._map.latLngToContainerPoint( [ point[0], point[1] ] );
      ctx.beginPath();
      ctx.fillStyle = colors[i];
      ctx.arc( p.x, p.y, r, 0, Math.PI * 2 );
      ctx.fill();
      ctx.closePath();
    }
  }
}
</script>



まず L.CanvasLayer.js を読み込みます。そして L.imageOverlay を使って画像(3600x3600.png)をロードするのですが、ここまでは前回紹介したものと同様です。

その後で L.canvasLayer() でキャンバスをレイヤーとして扱います。これでキャンバスの再描画イベントハンドラである onDrawLayer() をカスタマイズすることで画像に更にグラフィックをカスタマイズすることが可能になります。

このコードが実際に動いているワーキングデモページはこちらです:
https://dotnsf.github.io/leaflet_overlay2/


上記 URL をブラウザで開くと、前回同様「家にいるアマビエ」の画像が Leaflet の UI で表示されます:
2020050401


左上のズームコントローラーやスマホのピンチイン/アウト操作で画像が拡大縮小されます。実は前回と少しだけ違う点があることに気付く方はいらっしゃるでしょうか?:
2020050402


よく見ると窓とアマビエの右目尻に丸いオブジェクトが描画されています。これはゴミではなく、画像が表示されている Canvas の上にグラフィックコンテキストを使って(JavaScript で)描画されたものです:
2020050403


画面を更に拡大/縮小しても、ズームレベルに合わせて表示されます:
2020050404


この部分は上記コードの OnDrawLayer 関数内で処理されています。キャンバスが再描画されるタイミングで呼び出されますが、パラメータ(info)の info.canvas で表示されている <canvas> 要素を取得することができます。なので後は info.canvas.getContext( '2d' ) でグラフィックコンテキストを取得した後にここなどを参照して描画すると上述のように元の画像の上に更にグラフィックを追加した上で Leaflet の UI で表示できるようになる、というものです。


<canvas> をある程度使ったことがあると、この方法が使えるのは本当に便利です。自由に描画したキャンバスを Leaflet.js の UI で表示できるようになるため、使いみちが広がります。

オープンソースの地図ライブラリである Leaflet.js を使って(正確には「 Leaflet.js の UI 」を使って)、地図以外の情報を表示することができます。今回は「画像」を表示する方法を紹介します。

例えば Leaflet.js を使って OSM(OpenStreetMap) の地図を表示するには以下のような JavaScript コードをブラウザで実行します:
<div id="demoMap"></div>

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.1/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.3.1/dist/leaflet.js"></script>


<script>
var map = L.map('demoMap', { dragging: false, zoomControl: false, minZoom: 5, maxZoom: 5 }).setView( [ 35.695, 139.983 ], 5 );
L.tileLayer(
  'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    attribution: 'Map data © <a href="http://openstreetmap.org/">OpenStreetMap</a>',
    maxZoom: 5
  }
).addTo( map );
</script>

20200506


詳しくは紹介しませんが、青字の行でマップの定義を行って、赤字部分で OSM を指定して地図を表示する、という処理を実行しています。 この赤字部分ではタイルレイヤー(tileLayer)を使って OSM のタイルを指定しているのですが、Leaflet ではここを変えることで地図以外のオブジェクトを扱うことも可能です。今回はその中の1つである「画像」を扱う方法を紹介します。

まず Leaflet.js 利用時に限りませんが、地図アプリの UI というのはなかなか便利です。目的のオブジェクト(普通は地図)をマウスやスワイプでスクロールしたり、拡大・縮小したりして見たり、クリック・ダブルクリックによって特定位置を指定したりすることができます。これ自体が非常に便利な UI となっていて、地図以外でもこのようなインターフェースを使いたくなることがあります。今回は「(比較的大きな)画像ファイル」を対象に Leaflet.js でスクロール・拡大・縮小といったインターフェースが使えるように表示させてみます。

今回の表示対象画像は「いらすとや」「家にいるアマビエ」の画像とします:
amabie_stay_home


オリジナルは横 400 x 縦 373 ピクセルの画像だったのですが、これを 3600 x 3600 に拡大した画像(3600x3600.png)を用意しました。このファイルを使って以下のようなコードを用意します:
<div id="demoMap"></div>

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.1/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.3.1/dist/leaflet.js"></script>

<script>
//. 空の地図を OpenStreetMap データで表示
var map = L.map('demoMap', { dragging: true, zoomControl: true, minZoom: -5, maxZoom: 5, crs: L.CRS.Simple, preferCanvas: true } );

//. 表示する画像のパスとサイズ
var image = {
  url: './3600x3600.png',
  width: 3600,
  height: 3600
};

//. 画像領域を地図に設定
var imageBounds = L.latLngBounds([
  map.unproject( [ 0, image.height ] ),
  map.unproject( [ image.width, 0 ] )
]);
map.fitBounds( imageBounds );
map.setMaxBounds( imageBounds.pad( 0.5 ) );

//. 画像をオーバーレイで設定
L.imageOverlay( image.url, imageBounds ).addTo( map );
</script>

青字部分と赤字部分がそれぞれ元のコードに対応しています。ただ今回は赤字部分の前に画像を用意した後に、画像オーバーレイ(L.imageOverlay)を実行するための準備コードが間に含まれています。


実際にこのコードを使って動くページを用意しました。ワーキングデモはこちらを参照してください:
https://dotnsf.github.io/leaflet_overlay/


上記サイトに PC ブラウザかスマホブラウザでアクセスすると、画面中央に上述の画像が表示されます。ただ単に画像が表示されているわけではなく、画面左上に Leaflet.js のズームコントローラーが、右下には Leaflet ロゴが表示されています:
2020050201


ズームコントローラーを使うか、あるいはスマホなどでピンチイン・アウト操作を行うことで画像が拡大・縮小して表示できます:
2020050202


またマウスやスワイプ処理で画像をスクロール表示することも可能です:
2020050203


こういったユーザーインターフェースが簡単に作れてしまうという点で Leaflet.js は本当に便利です。


(このページの続きはこちらです)
http://dotnsf.blog.jp/archives/1077424924.html


IBM Cloudant(Apache CouchDB) の MapReduce ビューを使って、特定フィールドの値ごとの文書数を返す API を作ってみました。

なお以下の内容は IBM Cloudant でも Apache CouchDB でも同様に有効だと思っていますが、スクリーンショットなどは IBM Cloudant のものを使って説明しています。ご了承ください。


まず、前提として現状 Cloudant DB 内に以下のような JSON 文書が複数格納されているとします:
{
  "_id": "(id値)",
  "name": "(名前)",
  "date": "(日付)"
}

"name" フィールドに名前、"date" フィールドに日付文字列が格納されます。同じ "name" の値でも "date" の値は異なっていたり、同じ "date" の値でも "name" は異なっていたりするとします:
2020021401


この DB の状態から
 名前(name)ごとにグルーピングして、文書数がいくつずつあるか?
を調べる、というのが今回やりたいことです。

例えば上記例の場合であれば、"name" = "K.Kimura" の文書数は 5 、"name" = "K.Hashimoto" の文書数は 3 、"name" = "M.Matsuoka" の文書数は 2 、といった結果を導き出すための方法です。SQL の使える RDB であれば count() 関数と group by 句を使えば簡単そうですが、NoSQL 型である Cloudant でいちいち全件検索してから "name" の値ごとにカウントして・・・という REST API を作らずに調べるにはどうすればいいでしょうか?

その答が本ブログエントリのテーマでもある MapReduce ビューを作って、Cloudant REST API でこのビューを呼び出すことで実現できます。以下、その手順を紹介します。


まず DB 内に MapReduce ビューを定義するデザイン文書を作成します。画面左のメニュー "Design Documents" の+部分をクリックし、"New Doc" を選択します:
2020021402


新規にデザイン文書を追加する編集画面になるので、以下の内容を入力して "Create Document" ボタンをクリックします:
2020021401

{
  "_id": "_design/myindex",
  "language": "query",
  "views": {
    "count_by_name": {
      "map": {
        "fields": {
          "name": "asc"
        },
        "partial_filter_selector": {}
      },
      "reduce": "_count",
      "options": {
        "def": {
          "fields": [
            "name"
          ]
        }
      }
    }
  }
}

JSON の中身を一応解説すると、"myindex" という名前のデザイン文書を作り、その中で "count_by_name" という名前のビューを定義しています。このビューではまず "name" の値ごとにソート(map)し、その結果を _count 関数でカウント(reduce)した結果を値として持つよう定義しています。

正しく操作できていると Design Documents の中に定義した文書が追加されているはずです。これで MapReduce ビューが定義できました。
2020021404


後は Cloudant REST API でこのビューを呼ぶだけで結果を得ることができます。IBM Cloudant のホストURL (https://xxxx.cloudant.com)に続けて、DB 名(mapreduce)、デザイン文書名(myindex)とビュー名(count_by_name)を指定し、以下の URL にウェブブラウザでアクセスします:
https://xxxx.cloudant.com/mapreduce/_design/myindex/_view/count_by_name?group=true


すると以下のような結果が得られ、期待通りの結果を参照することができました:
2020021405
{
  "rows": [
    { "key" : [ "K.Hashimoto" ], "value" : 3 },
    { "key" : [ "K.Kimura" ], "value" : 5 },
    { "key" : [ "M.Matsuoka" ], "value" : 2 }
  ]
}

これで「DB 内にどんな名前の文書が存在しているか」や「各名前ごとの文書数」を簡単に調べることができるようになりました。

後はこのような処理を行う必要があるぶんだけビューを追加で定義しておけば、それぞれのビューごとに(フィールドとその値ごとに)文書数を調べたり、特定フィールド値の合計値を求めることができるようになります。


便利な地図表示ライブラリ Leaflet(と OpenStreetMap)を使って表示する地図は原則的には上が北になります。これを変更して(つまり地図を回転させて、上が北側にならないようにして)表示することに挑戦してみました。

最初は「やり方をググれば見つかるだろう」と軽く考えていたのですが、これはどうやらかなり難しいらしく、Stackoverflow で同様の質問を見つけましたが、ここでの回答には「Leaflet ネイティブで用意された方法は存在しない」「簡単な方法はない」とされています。かなりハードル高そうです。。

ネイティブには用意されていない、、簡単ではないものを自力で解決する、、 言うは簡単ですが実際には細かな調整含めて結構面倒なものでした。ただ一応サンプルとして動く形にできましたので、公開&紹介します。

まず、ソースコードはこちらに用意しました。実装は index.html ファイルを参照してください(バックエンドなしにこのファイル1つだけで実装しています):
https://github.com/dotnsf/om


実際に動く様子を確認・体験できるように github ページも用意しました。スマホのブラウザでこちらにアクセスしてください:
https://dotnsf.github.io/om/


なお、このページでは DeviceOrientation イベントをハンドリングしているため、環境によっては事前準備が必要です。こちらのページでも紹介しているように Android や iOS 12.1 以下の場合は事前設定不要です(ページを表示すればそのまま動きます)。 iOS 12.2 以上 13.0 未満の場合は URL 先にアクセスする前に予め Safari の設定で「モーションと画面の向きのアクセス」を ON にしておいてください(この設定後に URL にアクセスすれば動きます):
2020011001



また iOS 13.0 以上の場合は事前の準備は不要ですが、ページロード直後の初期状態で表示される「センサーの有効化」というボタンをクリックしてから「許可」してください:
2020011002


いずれの環境においても上記の準備を済ませた上でアクセスすると、東京を中心とした関東の地図が表示されます。また画面の中心には赤字でこのスマホデバイスの横方向への傾き角度が表示されます:
2020011701


スマホを横方向に傾けると、その傾きに合わせて地図が画面内で回転して表示されます。下図ではスマホを向かって右側に(時計回り方向に)傾けています。スマホの傾きに関係なく、常に真上(下図では左上方向)が北になります:
2020011700


実際に動かしている様子を動画にしました:



以下、技術的な解説を加えていきます。

まず CSS で body など地図を表示する要素のサイズを width も height も 100% に指定して、全画面で地図が表示されるようにしています。

そして回転処理そのものは地図を表示する <div> 要素に CSS の transform: rotate() 属性を適用して実現しています。今回の例では <div id="demoMap"> 内に Leaflet 地図を表示していますが、デバイスの傾き(角度: deg)を検知して、
 $('#demoMap').css( 'transform', 'rotate(-'+deg+'deg)' );
という処理を実行することで <div> ごと回転をかけて表示しています(加えて deg の数値を <div id="me"> 内で表示していますが、こちらは特別な処理をしていないため、詳細は省略します)。

ただこれだけだとうまくいきません。Leaflet の仕様なのか、CSS transform の仕様なのか「もともと表示されている要素が回転して表示される」らしく、縦 100% 横 100% の画面を回転させると、角度によっては表示されない部分が出来てしまうのでした:
2020011801



↑黒がスマホ本体(黒枠の中だけが実際に表示される)、左側は本体画面に地図が表示されている。スマホを回転させて表示すると、元の地図の矩形部分は表示されるが、表示しきれない部分(黄色の部分)が生じる。実際には黄色部分はグレーで表示されてしまいます。


この状況を回避するための工夫を加えています。具体的には <div id="demoMap"> のサイズを width, height ともに 200% に指定して実際のスマホ画面の4倍の面積のエリアをロードするようにしています(更に left と top を -50% にして、そのロードしたエリアの中心部分だけを表示するよう調整しています。こうすることで表示された画面の中心を軸に画面が自然に回転するようになります)。細かいことですが、この仕様に合わせて <div id="me"> の位置もこの変更に合わせて調整したり、(表示位置がズレないよう)画面にスクロールバーが出ないよう CSS で抑制したりしています:
2020011802


↑地図部分を実画面サイズよりも大きくすることで、回転させても表示不可エリアが生じないように調整しています。


こうした工夫によってなんとか実現できました。ただ地図上に書かれた文字が斜めになってしまうのはさすがにどうしようもないです。その辺りは地図 API 側でサポートしてくれるのを待つしかないかなあ。。

なお index.html 内の JavaScript 変数 lat(緯度), lng(経度)が地図の中心点座標、zoom がズームレベルです。これらの値を変えると表示するエリアや拡大縮小レベルを変えることができますので、自分の環境で挑戦してみたい方は是非挑戦してみてください。


マンホールマップなど、地図や位置情報を使ったアプリを何度か作ったことがあります。地図を扱うためのライブラリはいくつかありますが、leaflet.js というオープンソースのマップクライアントライブラリを使う機会がありました:
2017110901


leaflet.js 自体はオープンソースで提供された JavaScript によるマップ操作クライアントライブラリです。ここで扱うマップは OpenStreetMap だったり、国土地理院のものだったり、(プラグインを使えば)Google MAPs だったりを選ぶことができます。

使い方も簡単で、まずは(CDN などから)leaflet の css と javascript をロードしておきます(以下の例では leaflet 1.2.0 を使っています。また後で使うので jQuery もロードしています):
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.js"></script>

HTML 内にマップを表示する部分を定義します。この例ではブラウザ画面全体に広がる <div id="demoMap"></div> を定義しています:
<style>
html, body  {
	width: 100%;
	height: 100%;
	padding: 0px;
	margin: 0px;
}
#demoMap {
	width: 100%;
	height: 100%;
}
</style>
  :
  :
<body>
<div id="demoMap"></div>
</body>

そしてメイン部分。上記の demoMap 内に地図を表示します。以下の例では OpenStreetMap を使って千葉県船橋市役所周辺の地図を表示し、市役所の位置にマーカーを置いて、2秒おきにマーカーをランダムウォークさせる、というものです:
<script>
//. 船橋市役所の緯度経度(初期位置)
var lat = 35.69471100;
var lng = 139.98262100;

var map = null;
var marker = null;

$(function(){
  //. 船橋市役所を中心とした地図を OpenStreetMap データで表示
  map = L.map('demoMap').setView( [ lat, lng ], 15 );
  L.tileLayer(
    'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      attribution: 'Map data © <a href="http://openstreetmap.org/">OpenStreetMap</a>',
      maxZoom: 18
    }
  ).addTo( map );
  
  //. 初期状態で市役所にマーカーを設置
  marker = L.marker( [ lat, lng ] ).addTo( map );

  //. マーカーを2秒おきにランダムウォークさせる
  setInterval( randomWalk, 2000 );
});

function randomWalk(){
  //. マーカー位置をランダムに移動
  lat += Math.random() / 100.0 - 0.005;
  lng += Math.random() / 100.0 - 0.005;
  var latlng = new L.LatLng( lat, lng );
  marker.setLatLng( latlng );
}
</script>


完成品の HTML はこちらに用意しました。全体を確認したい方はこちらから HTML ファイルをダウンロードしてください:
https://raw.githubusercontent.com/dotnsf/samples/master/leafletjs.html


ダウンロードした HTML をウェブブラウザで表示すると、以下のように船橋市役所を中心としたマップが表示され、青いマーカーが2秒おきにその場所を少しずつ変えて移動する様子が確認できると思います:
2017110901


こういったライブラリを使って地図アプリを作っておくと、利用する地図を変える(例えば OpenStreetMap だったり、Google MAPs だったりに変える)のが楽になりますね。

このページのトップヘ