汎用画像ビューワとしてのLeaflet
Leaflet.jsはタイルサーバーにある地図タイルを表示するためのライブラリですが、表示できるのは地図だけではありません。
ひと工夫加えてやれば、地図以外にも手持ちの画像やデータなどを表示することができます。
Leaflet.jsはマウスを使って拡大・縮小や表示範囲の移動などインタラクティブな操作に長けており、汎用的な画像ビューワとして用いるのに事欠きません。
今回はフラクタル図形で有名なジュリア集合をLeafletに表示することができましたので、少し紹介させていただければと思います。
Leafletの動作のしくみ
まず、Leaflet.jsの動作原理について少し説明いたします。
Leaflet.jsのサンプルページをブラウザの開発ツールなどで見ながら読んでいただけるとわかりやすいのですが、
Leaflet.jsはcanvasベースのライブラリで、256px四方のcanvasを縦横に敷き詰めることで地図を表示しています。
それぞれのcanvasには経度(x)緯度(y)とズームレベル(z)の3次元の座標が紐付けられていて、この座標に基づいてLeaflet.jsはタイルサーバーから画像データを持ってきて対応するcanvasにそれを貼り付けることで地図が表示される仕組みになっています。
つまり、多数のcanvasの配置の管理と、canvasへの画像の表示がLeaflet.jsの主要なはたらきなのですが、後者に関してはプログラマが自由に操作することができまして今回はこれを行なっていこうと思います。
L.GridLayerを継承して独自クラスを作成
地図タイルを表示する際に用いるL.TileLayer
というクラスがありますが、兄弟クラスにL.GridLayer
というクラスがあります。
こちらを継承してクラスを作成し、createTile
メソッドを書き換えてやれば独自のレイヤーを作成することができます。
TypeScriptで書くと、たとえば次のような感じになります。
class MyCustomLayer extends L.GridLayer {
protected createTile(coords: L.Coords): HTMLElement {
// canvas要素を作成
let tile = L.DomUtil.create("canvas")
// タイルサイズをcanvasに設定
const tileSize = this.getTileSize()
tile.width = tileSize.x
tile.height = tileSize.y
// なんらかの描画処理
// .....
// .....
// canvasを返す
return tile
}
}
createTile
メソッドの引数coords
にはそのタイルの3次元座標が渡され、coords.x
のようにアクセスすることができます。
そして作成したクラスからレイヤーインスタンスを作成し地図に追加するには次のようにします。
const map = L.map("map")
const myCustomLayer = new MyCustomLayer()
map.addLayer(myCustomLayer)
このようなクラスを作成することで座標に応じてタイルを自前にレンダリングしたレイヤーを作成することができます。
Leaflet.js公式リファレンスに継承の例が多数書かれていますので、そちらもご覧ください。
ジュリア集合を描画
ここまでの手順でタイルに自由に好きなものを表示させることができるようになりましたので、次はジュリア集合を計算してcanvasに表示する部分を作っていきます。
大まかな手順としては、
- ジュリア集合を計算
- 1pxごとにRGBAを収録した
Uint8ClampedArray
を作成 -
ImageData
に変換 -
putImageData
メソッドでcanvasに描画
という流れで行なうのですが、ジュリア集合の計算は表示領域の全ての点に関して再帰的な計算を何十、何百と繰り返さなければいけないため、非常に負荷がかかります。
そのためジュリア集合の描画ロジックは、JavaScriptではなくRustを使って記述しWebAssemblyにコンパイルしたものをLeaflet.jsから呼び出して用いるようにしました。
描画ロジックはwasm-bindgenのチュートリアルページにあるものをベースに改造を加えたものを使用しました。具体的には、複素平面上で領域を指定してあげるとその部分だけを描画したデータを取得できるように修正しました。この改造により、Leaflet.jsでのズームイン・ズームアウト操作に対応して細部がレンダリングされるようになりました。
描画ロジックはnpmパッケージとしてまとめてあります。もしよろしければこちらもご覧ください。
Leafletでジュリア集合を表示
最終的に以下のような形になりました。
import init, { JuliaSet } from '@toriyama/draw-julia'
import * as L from "leaflet"
interface Complex {
re: number
im: number
}
export class JuliaSetLayer extends L.GridLayer {
julia: JuliaSet
constructor(constant: Complex, gridLayerOptions: L.GridLayerOptions) {
super(gridLayerOptions)
this.julia = JuliaSet.new(constant)
}
protected createTile(coords: L.Coords): HTMLElement {
// タイルのサイズを取得
const tileSize = this.getTileSize()
// ジュリア集合を描画する<canvas>要素を作成
let tile = L.DomUtil.create("canvas")
tile.width = tileSize.x
tile.height = tileSize.y
// 描画
this.julia.draw(tile, {
south: coords.y / Math.pow(2, coords.z),
north: (coords.y - 1) / Math.pow(2, coords.z),
west: (coords.x) / Math.pow(2, coords.z),
east: (coords.x + 1) / Math.pow(2, coords.z),
})
return tile
}
static async init() {
await init();
}
}
使い方は以下のようになります。L.map
でLeaflet.jsのコンテナーを作成する際に、crsオブションにL.CRS.Simple
を指定してやることがポイントです。
また、WebAssemblyを読み込むためにJuliaSetLayer.init
メソッドを必ず、レイヤー作成より前に呼び出す必要があります。
const div = document.getElementById("viewer")
const viewer = L.map(div, {
center: [0, 0],
zoom: 0,
crs: L.CRS.Simple
})
await JuliaSetLayer.init()
const juliaSetLayer = new JuliaSetLayer({
re: -0.15,
im: 0.65
})
viewer.addLayer(juliaSetLayer)
こちらもnpmパッケージとして公開しています。もしよろしければご覧ください。
- https://github.com/YUUKIToriyama/Leaflet.Fractal.Julia
- https://www.npmjs.com/package/leaflet.fractal.julia