Leafletにフラクタル図形を表示して楽しむ

![Julia set on Leaflet.js](https://camo.githubusercontent.com/93d0899dd66fc923c459768bc4cd88cec46ce750bc5c9bcee030dc7bc86dbd06/68747470733a2f2f692e696d6775722e636f6d2f4a7532555948322e706e67)

# 汎用画像ビューワとしての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で書くと、たとえば次のような感じになります。

```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`のようにアクセスすることができます。
そして作成したクラスからレイヤーインスタンスを作成し地図に追加するには次のようにします。

```typescript
const map = L.map("map")
const myCustomLayer = new MyCustomLayer()
map.addLayer(myCustomLayer)
```

このようなクラスを作成することで座標に応じてタイルを自前にレンダリングしたレイヤーを作成することができます。

[Leaflet.js公式リファレンス](https://leafletjs.com/examples/extending/extending-2-layers.html)に継承の例が多数書かれていますので、そちらもご覧ください。

## ジュリア集合を描画

ここまでの手順でタイルに自由に好きなものを表示させることができるようになりましたので、次はジュリア集合を計算してcanvasに表示する部分を作っていきます。
大まかな手順としては、
  1. ジュリア集合を計算
  2. 1pxごとにRGBAを収録した`Uint8ClampedArray`を作成
  3. `ImageData`に変換
  4. `putImageData`メソッドでcanvasに描画

という流れで行なうのですが、ジュリア集合の計算は表示領域の全ての点に関して再帰的な計算を何十、何百と繰り返さなければいけないため、非常に負荷がかかります。
そのためジュリア集合の描画ロジックは、JavaScriptではなくRustを使って記述しWebAssemblyにコンパイルしたものをLeaflet.jsから呼び出して用いるようにしました。

描画ロジックはwasm-bindgenの[チュートリアルページ](https://rustwasm.github.io/wasm-bindgen/examples/julia.html)にあるものをベースに改造を加えたものを使用しました。具体的には、複素平面上で領域を指定してあげるとその部分だけを描画したデータを取得できるように修正しました。この改造により、Leaflet.jsでのズームイン・ズームアウト操作に対応して細部がレンダリングされるようになりました。

描画ロジックはnpmパッケージとしてまとめてあります。もしよろしければこちらもご覧ください。
- https://github.com/YUUKIToriyama/draw-julia
- https://www.npmjs.com/package/@toriyama/draw-julia

## Leafletでジュリア集合を表示

最終的に以下のような形になりました。

```typescript
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`メソッドを**必ず**、**レイヤー作成より前に**呼び出す必要があります。

```javascript
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


## 参考リンク
- https://ja.wikipedia.org/wiki/%E3%82%B8%E3%83%A5%E3%83%AA%E3%82%A2%E9%9B%86%E5%90%88
- https://leafletjs.com/examples/extending/extending-2-layers.html
- https://rustwasm.github.io/wasm-bindgen/examples/julia.html