OpenStreetMapの地図レンダリング環境を自前で整える


はじめに

どうものらぬこです。

ちょっとやりたいことがあったので、個人所有のRapberry Pi 4に、OpenStreetMapの地図データ + PostgreSQL(with postGIS) + mapnik な環境と、データをいい感じに処理するためのフロントプログラミング環境として PHP + php-mapnik を導入してみました。

OpenStreetMapは、だれでも無償で利用ができる世界地図データを提供することを目的としたプロジェクトです。
地形データはもちろん、道路、鉄道、建物などのデータも格納されています。

地図データはxml形式のデータとして提供されており、日本のデータだけでも約13Gbの容量があります。

このデータを使えば、地名や建物などの検索もできる地図表示プログラムを作ったり、路線案内のようなツールを作ることも不可能ではありません。さらに、例えばgoogle mapでは、画像として保存されたgoogle mapの地図を公開、配布することは規約で禁止されていますが、OpenStreetMapではそのような制約もありません。

ただし、無償で提供されているのは地図のデータファイルのみであり、地図のレンダリングや検索に必要なサーバリソースなどがすべて無料で提供されているわけではありません(一部、無料で使える範囲も存在します)。

OpenStreetMapを使って表示される地図がどのようなものなのか少し触ってみたい、とか、パンフレット掲載用に開催場所周辺の地図画像が欲しい、等の目的であれば、OpenStreetMapの公式サイトを活用することができます。しかし、大勢の人が使うアプリに地図表示機能を組み込みたい、とか、たくさんの場所の周辺地図画像を一括で用意したい、等の目的には、公式サイトの地図機能を利用することはできません。

そのような目的でOpenStreetMapの地図データを利用したい場合には、基本的にはOpenStreetMapの地図を有料でホスティングしているサーバを使用するか、自分でサーバを用意するかの2択になります。

OpenStreetMapサーバーを自前で用意するための記事は探せば結構出てくるのですが、情報が古かったり、環境の違いなどによりうまく動かなかったりと、オープンソースをいろいろ組み合わせて環境準備する系のあるあるな罠盛りだくさんで、意図通りに動作させるまでにはかなりの苦労を強いられました。

僕が環境構築に利用した環境は以下の通りです。
Raspberry pi 4(4Gb) + Raspbian Buster with desktop(2019-09-26)
(最初にRaspberry pi 3BでチャレンジしたのですがpostgreSQLにデータをインポートする処理がメモリー不足が原因で完了せず、失敗に終わりました)
ストレージのmicroSDは、128Gbのものを利用しています(環境構築に45Gbほどの容量を使用するため、最低でも64Gbは必要です)

ちなみに、RaspbianはDebian系Linuxのため、DebianやUbuntu等であれば、x86, x64系PC、AWS上のEC2インスタンス環境などでも、パッケージのバージョンなどで多少の差異は出てくるかもしれませんが、大体同等の手順で環境が用意できるかと思います。

また、この記事では、OpenStreetMap地図データ(日本)をPostgresql+postGISな環境にインポートし、日本国内の指定された範囲の地図をmapnikを使ってpng出力できるところまでをやります。

google mapのような操作感の地図表示サイトを立ち上げることもできるのですが、mod_tileを使ったタイルサーバに仕立て上げ、leafletjsを利用してgoogle mapのような地図アプリを構築するというようなお話は扱いませんのでご了承ください。

これ以降は、Rasbianが導入済のRaspberry Pi 4を前提として話を進めます。

OpenStreetMapの地図サーバを用意する

ユーザの追加

まずは作業用ユーザを作ります。導入中、頻繁にroot権限を利用するため sudoグループにも追加しておきます。

# adduser osm
ユーザ `osm' を追加しています...
新しいグループ `osm' (1001) を追加しています...
新しいユーザ `osm' (1001) をグループ `osm' として追加しています...
ホームディレクトリ `/home/osm' を作成しています...
`/etc/skel' からファイルをコピーしています...
新しいパスワード:
新しいパスワードを再入力してください:
passwd: パスワードは正しく更新されました
osm のユーザ情報を変更中
新しい値を入力してください。標準設定値を使うならリターンを押してください
        フルネーム []: 
        部屋番号 []: 
        職場電話番号 []: 
        自宅電話番号 []: 
        その他 []: 
以上で正しいですか? [Y/n] Y
# usermod -G sudo osm

postgresql のインストール

次に、postgreSQL + postGIS を導入します。

# apt install postgresql postgis

apt でインストールした場合には、postgreSQLは11が、postGISは2.5系がインストールされます

OpenStreetMap用のデータベースを作成

OpenStreetMapのデータを格納するためのデータベースを作成し、postGIS拡張を有効化します。
postgreSQLのユーザは作業用ユーザアカウントと合わせてください(もしくは、pg_hba.confに osmユーザがパスワード無しでデータベースに接続できるような設定を記載してください)。

$ sudo su -
# su postgres
$ createuser osm
$ createdb -E UTF8 -O osm gis
$ psql -c "CREATE EXTENSION hstore;" -d gis
$ psql -c "CREATE EXTENSION postgis;" -d gis
$ exit

地図データをpostgreSQLに登録する

OpenStreetMapの地図データをウェブからダウンロードし、そのデータをpostgreSQLのデータベースに格納します。

$ wget -c http://download.geofabrik.de/asia/japan-latest.osm.pbf
$ wget https://github.com/gravitystorm/openstreetmap-carto/archive/v4.24.1.tar.gz
$ sudo apt install osm2pgsql
$ osm2pgsql --slim -d gis -C 1600 --hstore -S openstreetmap-carto-4.24.1/openstreetmap-carto.style japan-latest.osm.pbf

japan-latest.osm.pbf は日本の地図データで、前項でも少し書きましたが、この中身は10Gb超えのxmlファイルです(この記事を書いている時点では13Gbあります)。

openstreetmap-cartoは、地図をレンダリングするために使用するスタイルシート的な情報が書かれたプロジェクトです。
道路の色や線路の見た目、地図記号のアイコン情報などが所定の形式で格納されています。
このプロジェクトには、地図データの取得方法なども定義されていて、postgreSQLの接続設定情報、データを取得するためのSQL文なども情報もこの中に記載されています。

そして、osm2pgsqlというのが、OpenStreetMapのxmlデータをpostgreSQLにインポートするためのプログラムです。

osm2pgsqlの実行はものすごく時間が借ります(RaspberryPi4では一晩以上)。途中、コンソール出力がしばらく更新されなくなるタイミングがありますが、プログラムの実行はおそらく続いています。CPU占有率やps axでプロセス状態を確認しつつ気長に待ちましょう。

プログラムが終了したら、openstreetmap-cartoディレクトリ内に置かれている indexes.sql を実行し、データベースに追加のindex を作成します。これにもかなりの時間がかかりますが、やっておかないと、あとで地図データの検索にとてつもない時間を要することになります。

$ psql -d gis -f indexes.sql

なお、openstreetmap-cartoのプロジェクトは、また後程使用します。削除せずそのまま残しておいてください。

mapnikライブラリのインストール

次に、mapnikのインストールを行います。
mapnikもaptからインストールできるのですが、日本語fontの設定がうまくいかなかったため、こちらはソースからビルドしています。
また、mapnikが依存するfreetypeもソースからビルドしています。aptでインストールされるバージョンにはビルドに必要な ft-config というプログラム(?)が含まれていないためです。

$ sudo apt install libpq-dev libboost-dev libboost-regex-dev libboost-filesystem-dev libboost-system-dev libboost-program-options-dev libharfbuzz-dev libbz2-dev

$ wget https://sourceforge.net/projects/freetype/files/freetype2/2.10.1/freetype-2.10.1.tar.gz
$ tar xvzf freetype-2.10.1.tar.gz 
$ cd freetype-2.10.1/
$ ./configure
$ make
$ sudo make install

$ git clone https://github.com/mapnik/mapnik.git
$ cd mapnik
$ git checkout v3.0.22
$ git submodule update --init
$ ./configure FREETYPE_LIBS=/usr/local/lib FREETYPE_INCLUDES=/usr/local/include/freetype2
$ make
$ sudo make install 

プログラミング環境(php7-mapnik)のインストール

mapnikはライブラリモジュールのため、mapnikの機能を利用して地図のレンダリングを行うには、何らかの言語でプログラムを書く必要があります。
mapnikは、ネイティブのC/C++のほか、Ruby/Java/Ptython/PHP等からも利用できるように、それぞれの言語に対応したbindingも存在します。
僕は、使い慣れているPHPのbindingを選択しましたが、利用する際は、ご自分の使いたい言語向けに提供されているbindingを選択なさるのが良いかと思います。

以下は、php7-mapnikの導入方法となります。

何はともあれ、まずはPHPをインストールします。
拡張モジュールのビルドも行うため、php-devも併せてインストールします。

# apt install php php-dev

aptでインストールした場合、php7.3系がインストールされます。

次に、php7-mapnikをインストールします。

$ git clone https://github.com/garrettrayj/php7-mapnik.git
$ cd php7-mapnik
$ git checkout 2.0.0
$ phpize
$ ./configure
$ make 
$ make install

php7-mapnik のビルドにはgcc5以上が必要なため、CentOS7等、gccのバージョンが少し古めのディストリビューションではgccのアップグレードが必要になるかもしれません。

レンダリング用スタイルシート(openstreeetmap-carto)の設定

実際に地図をレンダリングするには、地図データの格納場所の設定(postgresqlサーバの設定)、地図の見た目(道路の色、線路の色、地図記号、地名などの表示用フォントなどなど)の設定が必要になります。

この情報を保持しているのが、osm2pgsql を実行する際にダウンロードした、openstreetmap-cartoというプロジェクトです。

地図内の地名などの表記もmapnikライブラリの機能で、画像としてレンダリングされます。

  • 日本語フォントの設定
  • 設定情報のビルド(openstreetmap-carto で定義されたデータから mapnik の設定データ(.xml)を生成する)。
  • 大陸・島々の海岸線データの生成

順にやっていきます。

以下、openstreetmap-carto ディレクトリ内で作業を行います。

$ cd openstreetmap-carto-4.24.1

レンダリングに使用するフォントの種類は、fonts.mssというファイルで定義されています。
日本語フォントの設定も記載されており、日本語のレンダリングにはNotoフォントというgoogleが作ったフォントを使用する設定となっています。ただ、日本語フォント本体はパッケージには含まれておらず、別途ダウンロードする必要があります。

Notoフォントのアーカイブはhttps://www.google.com/get/noto/ からダウンロードできます。
対応しているすべての言語のフォントが含まれているため、アーカイブのファイルサイズも大きめ(1.1Gb)です。

ファイルをダウンロードし、所定の場所(/usr/local/lib/mapnik/fonts/)に解凍すれば、地図レンダリング時に日本語が正しく表示されるようになる、、、はず、なのですが、提供されているフォント形式が.otf形式のためか、自分の環境ではうまくいきませんでした。

無償で提供されているTTF形式の日本語フォントのひとつ、みかちゃんフォントを使用することにしました(http://www001.upp.so-net.ne.jp/mikachan/)。サイトでは、ttf形式、ttc形式、otf形式の3種類を配布していますが、ttf形式をダウンロードしてください(だうんろーど→Windows用→ひとつづつ欲しい方はこちら から入手)。
なお、ダウンロードした.ttfファイルは、 /usr/local/lib/mapnik/fonts/ 以下に置いてください。

そして、fonts.mss を以下の内容で置き換えます。

@book-fonts:    "mikachan-P Regular";
@bold-fonts:    "mikachan-PB Regular",
@oblique-fonts: @book-fonts;

次に、地図のレンダリング設定を、mapnikの設定ファイル形式(.xml)に変換します。

$ sudo apt install npm
# sudo npm install -g carto
$ carto project.mml > mapnik.xml

carto 実行時にWarningがたくさん出力されますがスルーしておきます。

最後に、大陸・島々の海岸線データを準備しておきます。

openstreetmap-cartoプロジェクトディレクトリ内にスクリプトが用意されているので、これを呼び出すだけです。

$ python scripts/get_shapefile.py

以上で、すべて完了です。

地図のレンダリング

たとえば以下のようなスクリプトを動かせば、コマンドラインパラメータで指定された緯度、経度を中心とした 10km四方の地図がレンダリングされ、map.pngという名前で保存されます。

rendermap.php
<?php

ini_set('display_errors', 0);

$distance = 10;
$sizeImage = 500;

$lat = $argv[1];
$lng = $argv[2];
$latInterval = 360 / (6357 * 2 * 3.1416) * $distance;
$lngInterval = 360 / (6378 * cos(deg2rad($lat)) * 2 * 3.1416) * $distance;

$topLat = $lat - $latInterval / 2;
$bottomLat = $lat + $latInterval / 2;
$leftLng = $lng - $lngInterval / 2;
$rightLng = $lng + $lngInterval / 2;

$source = new \Mapnik\Projection('+init=epsg:4326');
$destination = new \Mapnik\Projection('+init=epsg:3857');
$transform = new \Mapnik\ProjTransform($source, $destination);
$boundingBox = new \Mapnik\Box2D(
    $leftLng,
    $topLat,
    $rightLng,
    $bottomLat);
$tileBoundingBox = $transform->forward($boundingBox);

$pluginConfigOutput = [];
exec('mapnik-config --input-plugins', $pluginConfigOutput);
\Mapnik\DatasourceCache::registerDatasources($pluginConfigOutput[0]);

$map = new \Mapnik\Map($sizeImage, $sizeImage, '+init=epsg:3857');

$fontConfigOutput = [];
exec('mapnik-config --fonts', $fontConfigOutput);
$map->registerFonts($fontConfigOutput[0]);

$exampleXmlPath = realpath(dirname(__FILE__)) . '/mapnik.xml';
$basePath = realpath(dirname(__FILE__));

$map->loadXmlFile($exampleXmlPath, false, $basePath);
$map->zoomToBox($tileBoundingBox);

$image = new \Mapnik\Image($sizeImage, $sizeImage);
$renderer = new \Mapnik\AggRenderer($map, $image);
$renderer->apply();
$renderedImage = $image->saveToString('png');
file_put_contents('map.png', $renderedImage);
$ php rendermap.php 36.0 134.0

環境構築にかかった時間

環境構築に要した時間は、諸々の調査、試行錯誤の時間を含めて3日ほど(土日含む)です。
待ち時間もそれなりで、postgreSQLへのデータインポートには一晩程度、mapnikライブラリのビルドにも1時間ほどの時間が掛かりました。

ちなみに、RaspberryPi4で試す前に、raspberry pi 3B で同じことをやろうとしていたのですが、メモリー不足によりpostgreSQLへのインポートが正常終了せず、失敗に終わっています。

何に使ったの?

現在、一人で泊まれる宿検索|ソロ旅ねっと というサービスを個人で運営しているのですが、登録されている約22000件の宿それぞれの周辺広域地図を静的ファイルとして用意したかった、という目的でこの環境を用意しました。
検索結果|ソロ旅ねっと

指定された範囲の地図画像を、任意の倍率でpngフォーマットで保存できるような機能は、実はOpenStreetMapの公式サイトにも備わっています。しかし、バッチプログラム等から機械的に大量の地図画像を取得するような用途で利用することはできません(利用規約で禁止されています)。
そこで、自分の好きに使えるように、自前で環境構築をしてみることにした、というのが理由となります。

今回は、縦横234ピクセルの300km四方の広域地図22000枚をpngフォーマットの画像ファイルとして保存したのですが、RaspberryPi4で4日以上はかかっています。