0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

onyxレポジトリを理解する

Posted at

はじめに

こちらの記事でnode.jsでベクトルタイルサーバを作成するためのコードについて記載しました。上記記事では、一つのMBTilesファイルを想定していましたが、ホスティングするデータが大きくなってくると、データを複数のMBTilesファイルに分割してホスティングすることがあります。
un-vector-tile-toolkit/onyx では、複数に分けられたMBTilesファイルからベクトルタイル配信を行うためのルーティングが実装されています。本記事では、onyxのコード解説を行い、実際にコードを実行してみます。
作業レポジトリはこちらです。

app.jsのコード

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)

コード解説

app.js
let tz = config.get('tz')

config/default.hjsonにおいて、例として以下のように記載されています。
tz: {
tapioca: 6
}
つまり、
tz = { tapioca: 6 }
などとなります。

app.js
if (!tz[t]) tz[t] = defaultZ

ここでの「t」は「mbtilesPath = `\${mbtilesDir}/${t}/0-0-0.mbtiles`」などとフォルダ名として利用されます。tは上記で見た通り、「tapioca」等です。
もし、mbtilesファイルの上位のフォルダが存在しない場合(つまり、tz[tapioca]などが定義されていない場合)には、tz[t]の値を 6 ( = defaultZ)と設定しています。

app.js
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
となります。

実際にタイル座標確認ページで確認しました。
ズームレベル9
スクリーンショット 2024-10-22 16.41.02.png

ズームレベル6
スクリーンショット 2024-10-22 16.41.14.png

確かに
z = 9, x = 455, y = 200
のタイルは、
z = 6, x = 56, y = 25
のタイルに含まれています。

つまり、このコードでは大きなズームレベルでのタイル座標を、tz[t]のズームレベルでの適切なタイル座標に変換しています。

app.js
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
を見てみたところ以下の通り表示されました。

スクリーンショット 2024-10-24 10.34.10.png

サーバでホスティングする

以前にこちらの記事でサーバでのホスティングを記載しました。今回は、こちらのサーバを用いて、onyxレポジトリのコードをホスティング出来るか試します。
作成したファイル群をFileZillaを用いてサーバに移動させて、npm installでモジュールをインストールしてから、node app.jsとします。
mapOs-mbtiles.html
を見てみると、chromeでは5回に1回くらいの割合で正常に表示されました。なぜか今回は、Edgeでは表示出来ませんでした。

まとめ

データを複数のMBTilesファイルに分割してホスティングしているonyxレポジトリのコードを解説して、レンタルサーバで実際にホスティングをしました。ブラウザによって不安定な挙動となることが分かりました。

Reference

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?