gzipベクトルタイルを作成してホスティングする

# はじめに

gzipなし（圧縮なし）のベクトルタイルデータをGitHubでホスティングすることは出来ていました。gzipありのMbtilesデータについては、サーバを立ててホスティングしました。ここでは、gzipありのpbf形式のベクトルタイルをホスティングする方法を検討してみます。

# gzipつきのベクトルタイルを作成する

[こちらの記事](https://qiita.com/k96mz/items/1f7e0f8e6e41e6a78cc6)では、 pbfベクトルタイルを圧縮形式にしない方式でデータを作成しました。ここでは、同様のデータを用いて、shpファイルからGeojsonを作成し、そこからベクトルタイルへ変換したいと思います。ソースデータ、ベクトルタイルの構成は同じです。

### shpからGeojsonへの変換

```
ogr2ogr -f GeoJSON coastl.geojson ne_110m_coastline.shp
ogr2ogr -f GeoJSON landmass.geojson ne_110m_land.shp
ogr2ogr -f GeoJSON riverl.geojson ne_110m_rivers_lake_centerlines.shp
```

### Geojsonからベクトルタイルへの変換

```
tippecanoe -e VTpractice --no-feature-limit --no-tile-size-limit --drop-rate=1 -Z0 -z4 -L coastl:coastl.geojson -L landmass:landmass.geojson -L riverl:riverl.geojson
```
--no-tile-compression
を含めないようにして、gzip圧縮として、ベクトルタイルを作成します。

# レンタルサーバでホスティング

Linux9のレンタルサーバでホスティングをします。フォルダ名を20250506gzipHostingとしました。
[こちらの記事](https://qiita.com/k96mz/items/d394f1ac0b71d80a93c0#https%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AE%E3%82%B3%E3%83%BC%E3%83%89%E4%BF%AE%E6%AD%A3)のコードを使用します。

以下はmbtiles用のコードなので削除します。
```js: appHttps.js
const VTRouter = require("./routes/VT");
app.use("/VT", VTRouter);
```

configファイルは以下のとおりです。

```default.hjson
{
  htdocsPath: htdocs
  port: 8080
  logDirPath: log
  morganFormat: tiny
  mbtilesDir: mbtiles
  privkeyPath: ./key/privkey.pem
  fullchainPath: ./key/cert.pem
}
```

keyフォルダを作成して、cert.pemとprivkey.pemを保存しました。

以下のコマンドで必要な最新版のパッケージをインストールします。
```
npm install @mapbox/mbtiles config cors express hh-mm-ss hjson morgan winston sqlite3 winston-daily-rotate-file spdy
```

[こちらの記事](https://qiita.com/k96mz/items/588a5241327f51cb5f9f#maplibre-gl-js%E3%81%A7%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B)のindex.htmlを利用します。

```html: index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Display a map</title>
    <meta
      property="og:description"
      content="Initialize a map in an HTML element with MapLibre GL JS."
    />
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link
      rel="stylesheet"
      href="https://unpkg.com/maplibre-gl@4.5.0/dist/maplibre-gl.css"
    />
    <script src="https://unpkg.com/maplibre-gl@4.5.0/dist/maplibre-gl.js"></script>
    <style>
      body {
        margin: 0;
        padding: 0;
      }
      html,
      body,
      #map {
        height: 100%;
      }
    </style>
  </head>
  <body>
    <div id="map"></div>
    <script>
      const map = new maplibregl.Map({
        container: "map", // container id
        style:
          "./download_style.json", // style URL
        center: [0, 0], // starting position [lng, lat]
        zoom: 1, // starting zoom
        maplibreLogo: true,
        hash: true,
      });

      //UI
      map.addControl(new maplibregl.NavigationControl(), "bottom-right");
      map.addControl(new maplibregl.ScaleControl());

      //debug
      map.showTileBoundaries = true;
      map.showCollisionBoxes = false;
    </script>
  </body>
</html>
```

スタイルファイルは上記の記事に含まれているdownload_style.jsonを利用します。
tilesの部分のみURLを変更しました。

```json: download_style.json
{
  "version": 8,
  "name": "Empty Style",
  "metadata": {"maputnik:renderer": "mlgljs"},
  "sources": {
    "v": {
      "type": "vector",
      "tiles": [
        "https://k96mz.xyz/normalVT/{z}/{x}/{y}.pbf"
      ],
      "minzoom": 0,
      "maxzoom": 4
    }
  },
  "sprite": "",
  "glyphs": "https://orangemug.github.io/font-glyphs/glyphs/{fontstack}/{range}.pbf",
  "layers": [
    {
      "id": "coastl",
      "type": "line",
      "source": "v",
      "source-layer": "coastl",
      "maxzoom": 5
    },
    {
      "id": "landmass",
      "type": "fill",
      "source": "v",
      "source-layer": "landmass",
      "maxzoom": 5,
      "paint": {"fill-color": "rgba(195, 176, 176, 1)"}
    },
    {
      "id": "riverl",
      "type": "line",
      "source": "v",
      "source-layer": "riverl",
      "maxzoom": 5,
      "paint": {"line-color": "blue", "line-width": 2}
    }
  ],
  "id": "dsyugj9"
}
```

# 地図を表示する

### gzip圧縮なし
まずは、gzip圧縮なしで表示できるか試します。（現在はサーバを立てていないので、アクセスできません。）
https://k96mz.xyz/index.html


無事に表示出来ました。

![スクリーンショット 2025-05-06 11.02.11.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/1f33250c-51d1-4b1f-83a6-6bf5a4fea430.jpeg)

### gzip圧縮あり

次にgzip圧縮ありの地図を表示します。
https://k96mz.xyz/index_gzip.html

![スクリーンショット 2025-05-06 11.13.55.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/898591bd-05de-462a-8355-375e586e9a4a.jpeg)

Status Codeが200 OKとなっているにも関わらず、地図が表示されません。

以下のメッセージがでています。
"Unable to parse the tile at https://k96mz.xyz/gzipVT/0/0/0.pbf, please make sure the data is not gzipped and that you have configured the relevant header in the server"

ベクトルタイル (.pbf) が gzip 圧縮されているのに、HTTP レスポンスに Content-Encoding: gzip ヘッダーが設定されていないことが原因のエラーです。
そのため、以下のとおり設定します。

```js: appHttps
app.use(express.static(htdocsPath, {
  setHeaders: (res, path) => {
    if (path.endsWith('.pbf')) {
      res.setHeader('Content-Encoding', 'gzip'); // 重要
      res.setHeader('Content-Type', 'application/vnd.mapbox-vector-tile');
    }
  }
}));
```

最終的なコードは以下のとおりです。

```js: appHttps.js
// Set Module
const config = require("config");
const express = require("express");
const cors = require("cors");
const morgan = require("morgan");
const winston = require("winston");
const DailyRotateFile = require("winston-daily-rotate-file");
const fs = require("fs");
const spdy = require("spdy"); //for https

// Configure constant
const port = config.get("port");
const htdocsPath = config.get("htdocsPath");
const morganFormat = config.get("morganFormat");
const logDirPath = config.get("logDirPath");
const privkeyPath = config.get("privkeyPath");
const fullchainPath = config.get("fullchainPath");

// Logger configuration
const logger = winston.createLogger({
  transports: [
    new winston.transports.Console(),
    new DailyRotateFile({
      filename: `${logDirPath}/server-%DATE%.log`,
      datePattern: "YYYY-MM-DD",
    }),
  ],
});

logger.stream = {
  write: (message) => {
    logger.info(message.trim());
  },
};

// Middleware
const app = express();
app.use(cors());
// const VTRouter = require("./routes/VT");
app.use(
  morgan(morganFormat, {
    stream: logger.stream,
  })
);
// app.use(express.static(`${__dirname}/${htdocsPath}`));
app.use(express.static(htdocsPath, {
  setHeaders: (res, path) => {
    if (path.endsWith('.pbf')) {
      res.setHeader('Content-Encoding', 'gzip'); // 重要
      res.setHeader('Content-Type', 'application/vnd.mapbox-vector-tile');
      // res.setHeader('Access-Control-Allow-Origin', '*'); // CORS対策
    }
  }
}));

// app.use("/VT", VTRouter);

// for http
// app.listen(port, () => {
//   console.log(`Starting server at port ${port}`);
// });

// for https
spdy
  .createServer(
    {
      key: fs.readFileSync(privkeyPath),
      cert: fs.readFileSync(fullchainPath),
    },
    app
  )
  .listen(port);
```


無事に表示されました。

![スクリーンショット 2025-05-06 11.25.59.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/07334503-4d32-4eb6-88eb-ce0849150e07.jpeg)

以下も正常に反映されています。
content-encoding:gzip
content-type:application/vnd.mapbox-vector-tile


# まとめ
gzipありのpbf形式のベクトルタイルをホスティングする方法をまとめました。HTTP レスポンスに Content-Encoding: gzip ヘッダーを設定することが重要です。

# Reference
https://qiita.com/k96mz/items/1f7e0f8e6e41e6a78cc6

https://qiita.com/k96mz/items/d394f1ac0b71d80a93c0#https%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AE%E3%82%B3%E3%83%BC%E3%83%89%E4%BF%AE%E6%AD%A3

https://qiita.com/k96mz/items/588a5241327f51cb5f9f#maplibre-gl-js%E3%81%A7%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B