はじめに
こちらの記事でnode.jsでベクトルタイルサーバを作成するためのコードについて記載しました。上記記事では、一つのMBTilesファイルを想定していましたが、ホスティングするデータが大きくなってくると、データを複数のMBTilesファイルに分割してホスティングすることがあります。
un-vector-tile-toolkit/onyx では、複数に分けられたMBTilesファイルからベクトルタイル配信を行うためのルーティングが実装されています。本記事では、onyxのコード解説を行い、実際にコードを実行してみます。
作業レポジトリはこちらです。
app.jsのコード
const config = require('config')
const fs = require('fs')
const express = require('express')
const spdy = require('spdy')
const cors = require('cors')
const morgan = require('morgan')
const MBTiles = require('@mapbox/mbtiles')
const TimeFormat = require('hh-mm-ss')
const winston = require('winston')
const DailyRotateFile = require('winston-daily-rotate-file')
// config constants
const morganFormat = config.get('morganFormat')
const htdocsPath = config.get('htdocsPath')
const privkeyPath = config.get('privkeyPath')
const fullchainPath = config.get('fullchainPath')
const port = config.get('port')
const defaultZ = config.get('defaultZ')
const mbtilesDir = config.get('mbtilesDir')
const fontsDir = config.get('fontsDir')
const logDirPath = config.get('logDirPath')
// global variables
let mbtilesPool = {}
let tz = config.get('tz')
let busy = false
// logger configuration
const logger = winston.createLogger({
transports: [
new winston.transports.Console(),
new DailyRotateFile({
filename: `${logDirPath}/onyx-%DATE%.log`,
datePattern: 'YYYY-MM-DD'
})
]
})
logger.stream = {
write: (message) => { logger.info(message.trim()) }
}
// app
const app = express()
app.use(cors())
app.use(morgan(morganFormat, {
stream: logger.stream
}))
app.use(express.static(htdocsPath))
const getMBTiles = async (t, z, x, y) => {
let mbtilesPath = ''
if (!tz[t]) tz[t] = defaultZ
if (z < tz[t]) {
mbtilesPath = `${mbtilesDir}/${t}/0-0-0.mbtiles`
} else {
mbtilesPath =
`${mbtilesDir}/${t}/${tz[t]}-${x >> (z - tz[t])}-${y >> (z - tz[t])}.mbtiles`
}
return new Promise((resolve, reject) => {
if (mbtilesPool[mbtilesPath]) {
resolve(mbtilesPool[mbtilesPath].mbtiles)
} else {
if (fs.existsSync(mbtilesPath)) {
new MBTiles(`${mbtilesPath}?mode=ro`, (err, mbtiles) => {
if (err) {
reject(new Error(`${mbtilesPath} could not open.`))
} else {
mbtilesPool[mbtilesPath] = {
mbtiles: mbtiles, openTime: new Date()
}
resolve(mbtilesPool[mbtilesPath].mbtiles)
}
})
} else {
reject(new Error(`${mbtilesPath} was not found.`))
}
}
})
}
const getTile = async (mbtiles, z, x, y) => {
return new Promise((resolve, reject) => {
mbtiles.getTile(z, x, y, (err, tile, headers) => {
if (err) {
reject()
} else {
resolve({tile: tile, headers: headers})
}
})
})
}
app.get(`/zxy/:t/:z/:x/:y.pbf`, async (req, res) => {
busy = true
const t = req.params.t
const z = parseInt(req.params.z)
const x = parseInt(req.params.x)
const y = parseInt(req.params.y)
getMBTiles(t, z, x, y).then(mbtiles => {
getTile(mbtiles, z, x, y).then(r => {
if (r.tile) {
res.set('content-type', 'application/vnd.mapbox-vector-tile')
res.set('content-encoding', 'gzip')
res.set('last-modified', r.headers['Last-Modified'])
res.set('etag', r.headers['ETag'])
res.send(r.tile)
busy = false
} else {
res.status(404).send(`tile not found: /zxy/${t}/${z}/${x}/${y}.pbf`)
busy = false
}
}).catch(e => {
res.status(404).send(`tile not found: /zxy/${t}/${z}/${x}/${y}.pbf`)
busy = false
})
}).catch(e => {
res.status(404).send(`mbtiles not found for /zxy/${t}/${z}/${x}/${y}.pbf`)
})
})
app.get(`/fonts/:fontstack/:range.pbf`, (req, res) => {
res.set('content-type', 'application/x-protobuf')
res.set('content-encoding', 'gzip')
for(const fontstack of req.params.fontstack.split(',')) {
const path = `${fontsDir}/${fontstack}/${req.params.range}.pbf.gz`
if (fs.existsSync(path)) {
res.send(fs.readFileSync(path))
return
}
}
res.status(404).send(`font not found: ${req.params.fontstack}/${req.params.range}`)
})
spdy.createServer({
key: fs.readFileSync(privkeyPath),
cert: fs.readFileSync(fullchainPath)
}, app).listen(port)
コード解説
let tz = config.get('tz')
config/default.hjsonにおいて、例として以下のように記載されています。
tz: {
tapioca: 6
}
つまり、
tz = { tapioca: 6 }
などとなります。
if (!tz[t]) tz[t] = defaultZ
ここでの「t」は「mbtilesPath = `\${mbtilesDir}/${t}/0-0-0.mbtiles`」などとフォルダ名として利用されます。tは上記で見た通り、「tapioca」等です。
もし、mbtilesファイルの上位のフォルダが存在しない場合(つまり、tz[tapioca]などが定義されていない場合)には、tz[t]の値を 6 ( = defaultZ)と設定しています。
if (z < tz[t]) {
mbtilesPath = `${mbtilesDir}/${t}/0-0-0.mbtiles`;
} else {
mbtilesPath = `${mbtilesDir}/${t}/${tz[t]}-${x >> (z - tz[t])}-${
y >> (z - tz[t])
}.mbtiles`;
}
z(リクエストのあったズームレベル)がtz[t]より小さい時(つまりzが5以下の時)は、mbtilesPathが以下のようになります。
mbtilesPath = /somewhere/mbtiles/tapioca/0-0-0.mbtiles
zがtz[t]以上の時(つまりzが6以上の時)は以下のようになります。
${x >> (z - tz[t])}
については、例えば z が 8 だとすると、
z - tz[t] = 8 - 6 = 2
となります。
この場合、
x >> 2
となり、xを右に2ビットだけシフトさせます。
つまり、xの値を2*2 = 4で割った値となります。
ビットシフト演算子 >> については、こちらの記事にまとめました。
例えば、つくば市辺りのタイルを考えて、z = 9, x = 455, y = 200とすると、
\${x >> (z - tz[t])} = 455 >> 3 = 455 ÷ 8 = 56.875
\${y >> (z - tz[t])} = 200 >> 3 = 200 ÷ 8 = 25
となります。
確かに
z = 9, x = 455, y = 200
のタイルは、
z = 6, x = 56, y = 25
のタイルに含まれています。
つまり、このコードでは大きなズームレベルでのタイル座標を、tz[t]のズームレベルでの適切なタイル座標に変換しています。
app.get(`/fonts/:fontstack/:range.pbf`, (req, res) => {
res.set('content-type', 'application/x-protobuf')
res.set('content-encoding', 'gzip')
for(const fontstack of req.params.fontstack.split(',')) {
const path = `${fontsDir}/${fontstack}/${req.params.range}.pbf.gz`
if (fs.existsSync(path)) {
res.send(fs.readFileSync(path))
return
}
}
res.status(404).send(`font not found: ${req.params.fontstack}/${req.params.range}`)
})
ルート設定 (/fonts/:fontstack/:range.pbf)
- このエンドポイントは /fonts/ に続く、2 つのパラメータ :fontstack と :range.pbf を受け取ります。
- fontstack: フォントの名前やスタイルのリスト(カンマ区切り)
- range.pbf: プロトコルバッファ形式のフォントファイル(.pbf拡張子付き)の特定の範囲(フォントの文字コード範囲など)
例: /fonts/Roboto-Bold,Arial/0-255.pbf
for文のところでは、フォントファイルを探す処理を行っていますが、詳しいことは良くわかりませんでした。分かったら、また追記したいと思います。
ローカルでホスティングしてみる
まずは、以下のようにして必要なモジュールをインストールします。
npm install @mapbox/mbtiles config cors express hh-mm-ss hjson morgan spdy winston winston-daily-rotate-file
先日、こちらの記事で作成したmbtilesをローカルにおいてホスティングしてみます。
エラー
if (fs.existsSync(mbtilesPath)) {
上記部分の処理がうまく出来ずに時間がかかりました。原因としては、default.hjsonにおいて、
mbtilesDir: /somewhere/mbtiles
としていたことです。正しくは、
somewhere/mbtiles
とする必要があります。つまり、最初のスラッシュが不要でした。
スタイルファイルはbndlだけ適当に用意しました。
node app-local.js
として、
http://localhost:3000/map/mapO-localmbtiles.html
を見てみたところ以下の通り表示されました。
サーバでホスティングする
以前にこちらの記事でサーバでのホスティングを記載しました。今回は、こちらのサーバを用いて、onyxレポジトリのコードをホスティング出来るか試します。
作成したファイル群をFileZillaを用いてサーバに移動させて、npm installでモジュールをインストールしてから、node app.jsとします。
mapOs-mbtiles.html
を見てみると、chromeでは5回に1回くらいの割合で正常に表示されました。なぜか今回は、Edgeでは表示出来ませんでした。
まとめ
データを複数のMBTilesファイルに分割してホスティングしているonyxレポジトリのコードを解説して、レンタルサーバで実際にホスティングをしました。ブラウザによって不安定な挙動となることが分かりました。
Reference