OpenStreetMapデータから地図を作成する

# 1. この記事について


## やりたいこと


任意のスタイルの地図を作成して動画や印刷物に使用したい


## やったこと


OpenStreetMapデータとCartoCSSスタイルからMapnikを使って地図画像を作成しました
この記事ではその手順について解説します


# 2. 技術選定


PCで地図を扱う場合には以下の2つのパターンがあると思います


1. ブラウザで閲覧するユーザー向けにズーム可能な地図アプリを提供する
2. 静止画として作成済みの地図画像をサイトや印刷物などに表示する


今回やりたいのは2です


## 2-1. ズーム可能な地図アプリを提供する


Google Mapsのように、縮尺別の地図タイルを動的に取得して並べて表示する方式です。オープンソースの選択肢としてはleafletがよく用いられていますが、有償サービスとしてGoogle Maps JavaScript API, MapBox APIなども選択できます。この方式では地図タイルを動的に取得しながら表示していく為、軽量な地図タイルを作成しておいてタイルサーバーで効率的に配信する必要があり、その運用コストを考えると既存の地図タイルを利用するインセンティブが大きくなりますが、自前でタイルを作成することもできます


### 生データから地図タイルを作成する手順


OpenStreetMapの生データから地図タイルを作成して表示する場合、以下のような手順になります


1. 地理データを準備する（OpenStreetMap生データなど）
2. DBに地理データを展開する（PostGIS, geopackagesなど）
3. 地図描画の為のスタイルを用意する（cartoCSSなど）
4. 地図タイルをレンダリングする（mapnikなど）
5. 地図タイルを並べて地図を表示する（leaflet, QGISなど）


## 2-2. 地図画像を作成する


印刷物に貼り付けるなどの用途であれば画像として出力できれば良いので選択できる方法が増えます。データソースとしては地図タイルを使っても良いですし、地理データとスタイルからレンダリングすることも出来ます。地図作成も、Mapnikなどの地図レンダラーを使う方法、GUIで操作できるGIS（Geographic Information System）を使う方法、Google MapsなどのAPIとして提供されている地図画像生成機能を使う方法などが選択できます


### 生データから地図を作成する手順


先ほどの手順とほぼ同じですが、タイルが不要なので直接地図をレンダリングできます


1. 地理データを準備する（OpenStreetMap PBFファイルなど）
2. DBに地理データを展開する（PostGIS, geopackagesなど）
3. 地図描画の為のスタイルを用意する（cartoCSSなど）
4. 地図画像をレンダリングする（mapnik, QGISなど）


## 2-3. 最終的に何を選定したか


今回はOpenStreetMap PBFファイル、PostGIS、cartoCSS, Mapnikで地図画像を作成します
ほぼ同じ手順で地図タイルを作成することもできます


# 3. 基本手順


## 3-1. OpenStreetMap地理データを入手する


OpenStreetMapはpbf（Protocolbuffer Binary Format）で地理データを公開していますが、QGISはこれをそのまま読み込んで表示することが出来ます。このpbfファイルはすべての情報を持っているので色々なスタイルで表示することが出来るのですが表示するには処理が必要になり、また容量が大きいのでそのまま扱うには適しません。日本全域のデータだけでも2.2GByteあるので表示されるまで数十分かかり、位置やズームを変更するだけで同じ時間待機することになりますので、PostGIS, Geopackage, タイルデータなどにして運用する方が良さそうです


地球全体のデータは以下から取得できますが150GByteあります
https://planet.openstreetmap.org/


以下から地域別データを取得できます
今回はjapan-latest.pbfをダウンロードして利用しました
https://download.geofabrik.de/


特定の地域だけのデータを取得するには以下でトリミングして取得できます
https://www.openstreetmap.org/export#map=12/34.9686/138.4770


## 3-2. 地理データをPostGISに展開する


上記のjapan-latest.pbfはXML的な構造を持つバイナリファイルです。こちらをそのまま利用すると遅くてやってられないので、DBに展開して利用するのが通常の手順となります。GISで利用する為のpostgreSQL拡張であるPostGISを作成します


PostGISは公式からDockerイメージが用意されているので簡単に環境を構築できます。IDパスワードも環境変数から設定できますので簡単です。今回はローカル環境で使うだけなのでテキトーですが外部に公開するような場合はちゃんと設定してください


```dockerfile:Dockerfile
FROM postgis/postgis

ENV DBNAME=postgres \
    DBUSER=postgres \
    DBPASS=postgres \
    DBHOST=postgis

RUN apt-get update && \
    apt-get install -y osm2pgsql && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*
```


先程ダウンロードしたjapan-latest.osm.pbfを/data/に配置してdocker execなどでCLIから以下を実行すればpbfファイルの内容をPostGISに展開することができます


```sh:docker exec -it
osm2pgsql \
    -d postgres \
    -H postgis \
    -P 5432 \
    -U postgres \
    --create \
    --slim \
    --hstore \
    --multi-geometry \
    --cache 2000 \
    /data/japan-latest.osm.pbf
```


使用するのが地球全域のデータであればこれで問題ないのですが、japan-latest.pbfを使用する場合は存在しないレイヤーやカラムがある為にOpenStreetMap公式のcartoCSSでエラーが出てしまいますので、以下を実行してテーブルやカラムを追加してやります


```sh:docker exec -it
for tbl in icesheet_polygons water_polygons simplified_water_polygons land_polygons icesheet_outlines ne_110m_admin_0_boundary_lines_land; do
  echo "Checking table '$tbl'"
  # if psql -h postgis -U postgres -d postgres -c '\dt' | grep -q "$tbl"; then
  if psql -h postgis -U postgres -d postgres -c '\dt' | grep -qwE '\b'"$tbl"'\b'; then
    echo "'$tbl' already exists. Skipping creation."
  else
    echo "Creating table '$tbl'"
    psql -h postgis -U postgres -d postgres -c \
      "CREATE TABLE $tbl (way geometry(Polygon, 3857));"
  fi
done

echo "Ensuring 'ice_edge' column exists in 'icesheet_outlines'..."
if ! psql -h postgis -U postgres -d postgres -c "\d icesheet_outlines" | grep -q "ice_edge"; then
  echo "Adding 'ice_edge' column..."
  psql -h postgis -U postgres -d postgres -c \
    "ALTER TABLE icesheet_outlines ADD COLUMN ice_edge text;"
else
  echo "'ice_edge' column already exists."
fi
```


## 3-3. スタイルを作成する



とりあえず表示してみる為に簡単なスタイルを作ってみましょう
postgisを参照してroadsレイヤーを単色で表示するだけの簡単スタイルです


```xml:style.xml
<Map>
  <Style name="roads">
    <Rule>
      <LineSymbolizer stroke="black" stroke-width="1" />
    </Rule>
  </Style>

  <Layer name="roads" srs="+init=epsg:3857">
    <StyleName>roads</StyleName>
    <Datasource>
      <Parameter name="type">postgis</Parameter>
      <Parameter name="host">postgis</Parameter>
      <Parameter name="port">5432</Parameter>
      <Parameter name="user">postgres</Parameter>
      <Parameter name="password">postgres</Parameter>
      <Parameter name="dbname">postgres</Parameter>
      <Parameter name="table">(SELECT way FROM planet_osm_line WHERE highway IS NOT NULL) as roads</Parameter>
      <Parameter name="geometry_field">way</Parameter>
      <Parameter name="srid">3857</Parameter>
    </Datasource>
  </Layer>
</Map>
```


## 3-4. Mapnikで地図をレンダリングする


Mapnik環境はLinuxでないと作りにくいのでdockerで作成しました


```Dockerfile:Dockerfile
FROM ubuntu:22.04

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get install -y \
    curl \
    python3 \
    python3-pip \
    python3-mapnik \
    mapnik-utils \
    fontconfig \
    fonts-noto \
    fonts-noto-cjk \
    fonts-noto-color-emoji \
    fonts-hanazono \
    && fc-cache -fv \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

WORKDIR /app

RUN pip3 install fastapi uvicorn jupyter

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
```


MapnikはC++で作成されていますがPythonで動きます
先ほど作成したstyle.xmlをmain.pyと同じディレクトリに置いてやればAPIとして動きます


```python:main.py
from fastapi import FastAPI, Query
from fastapi.responses import Response
import mapnik
import math

app = FastAPI()

@app.get("/map")
def render_map(z: int = Query(10), x: int = Query(1000), y: int = Query(1000)):
    m = mapnik.Map(256, 256)
    mapnik.load_map(m, "style.xml")  # Mapnik XMLファイルを用意しておく

    m.zoom_all()

    im = mapnik.Image(256, 256)
    mapnik.render(m, im)
    png = im.tostring("png")

    return Response(content=png, media_type="image/png")
```


地図が作成できました


![map5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/2a8cc46f-a08f-43e3-bd14-5378839aae6f.png)


# 4. CartoCSSを使う


Mapnikはxmlで記載したスタイルで描画しますが、80以上あるレイヤーすべてにそれぞれ縮尺別の条件を書くのは大変なので、OpenStreetMapではCSS的な記載が可能なCartoCSSで定義してXMLファイルにビルドする形で運用されています


## 4-1. osm-cartoを入手する


OpenStreetMapのCartoCSSが公開されています
https://github.com/gravitystorm/openstreetmap-carto


## 4-2. style.xmlをビルドする


ビルドに使うcartoコマンドはnode.jsで動きます


```node:terminal
npm install -g carto
```


project.mmlを指定してcartoコマンドを実行すればビルドしてくれます。project.mmlから各ファイルを参照しますが、ディレクトリ構造を変更していなければそのままで参照できるようになっています


```:terminal
carto /carto_css/project.mml > style.xml
```


ビルドされたstyle.xmlを先ほどのMapnik環境に置けばOpenStreetMapのスタイルで表示されます


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/eca24052-0ce8-4b67-be9a-8cca1ce3277e.jpeg" width=60%>


# 5. 海のデータを足す


japan-latest.osm.pbfは海のデータを含まないようなので、OpenStreetMapも利用しているNatural Earthのデータを足してやります
https://www.naturalearthdata.com/downloads/10m-physical-vectors/10m-ocean/


shpファイルなのでshp2pgsqlで変換してPostGISに登録します


```:terminal
shp2pgsql -I -s 3857 -g way /data/natural_earth/ne_10m_ocean.shp simplified_water_polygons | psql -h postgis -U postgres -d postgres
```


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/da078b39-a127-489e-ac04-f56086a60da8.jpeg" width=60%>


# 6. CartoCSSをカスタマイズしてデザインを変える



```ferry-routes.mss
@ferry-route: #66f;
@ferry-route-text: @ferry-route;

#ferry-routes {
  [zoom >= 8] {
    /* background prevents problems with overlapping ferry-routes, see #457 */
    background/line-color: @water-color;
    background/line-width: 1; /* Needs to be a bit wider than the route itself because of antialiasing */
    line-color: @ferry-route;
    line-width: 0.4;
    line-dasharray: 4,4;
    [zoom >= 11] {
      background/line-width: 1;
      line-width: 0.8;
      line-dasharray: 6,6;
    }
  }
}

#ferry-routes-text {
  [zoom >= 13] {
    text-name: "[name]";
    text-face-name: @book-fonts;
    text-placement: line;
    text-fill: @ferry-route-text;
    text-spacing: 1000;
    text-size: 10;
    text-dy: -8;
  }
}
```


こんな感じでデザインを変更できます
（ちょっとしか変わってません）


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/a39a4a86-505a-445e-a4f7-4b1817e6cc15.png" width=60%>


# まとめ


かなり苦労しましたがなんとか公式通りのやり方でデザインを導入することができました


MapnikやCartoCSSは結構手ごわいので、QGISのように色々なデータフォーマットを自動で読んでくれるツールを使って確認しながらやった方がやりやすいと思います


OpenStreetMapはかなり細かく要素分けして情報が書かれてますので、うまく使えばいろんな地図を書くことができて面白いです。レッツトライ