雨の日の屋根がある歩行者ルート検索をOpenStreetMapとZENRIN Maps APIで比較してみた

## 要約
本記事では、**ZENRIN Maps API** と**OpenStreetMap**（以下、OSM）を使用して屋根付きの歩行者ルートを検索・表示する方法を実装し、その違いを比較しました。
ZENRIN Maps APIでは屋根付きルートの検索が可能でしたが、OSMでは直接的な検索ができませんでした。
代替手段として屋根のある道路のタグ情報を参照して表示する方法を確認しました。両APIの特徴や実装の容易さ、カスタマイズ性について解説しています。

## はじめに

本記事では、ZENRIN Maps API と OSM を使用して屋根付きの歩行者ルートを検索・表示する方法を実装し、その違いを比較してみました。

### 対象読者
- JavaScriptで地図アプリケーションを開発したい方
- 経路探索に興味がある方
- OSMと商用地図APIの違いを知りたい方


この記事では、以下を解説します。

- **ZENRIN Maps API**を使用した屋根付きルート検索の実装
- **OSM**を使用したルート検索とその課題
- 実装したコードの説明と課題

## ZENRIN Maps APIを使用したルート検索

### 実装概要

ZENRIN Maps APIを利用して、特定の出発地点から目的地点までの歩行者ルートを検索し、屋根付きの区間を優先的に表示する機能を実装しました。
ZENRIN Maps APIを使用するためには検証用IDとPW取得が必要です。
お試しIDは下記から簡単に発行できました。
[ZENRIN Maps API　無料お試しID　お申込みフォーム（2か月無料でお試しできます）](https://www.zenrin-datacom.net/solution/zenrin-maps-api/trial?fm_cp=6757baf3203e3a00bb118509&fm_mu=6763954a0dbf8e0512d61054&utm_campaign=6757baf3203e3a00bb118509&utm_medium=else&utm_source=Qiita16)

以下は主な機能です

- **地図の初期化**：地図を表示し、出発地点と目的地点をマーカーで示します。
- **ルート検索**：APIを使用してルート情報を取得します。
- **屋根付き区間の表示**：取得したルート情報から、屋根付き区間をデコードして地図上に描画します。

#### 実装コード

以下は実装したコードです。

```javascript
ZMALoader.setOnLoad(function(mapOptions, error) {
    if (error) {
        console.error('ZMALoader error:', error);
        return;
    }

    const mapElement = document.getElementById('map');

    mapOptions.center = new ZDC.LatLng(35.71195756, 139.79494287);

    mapOptions.zoom = 17;

    const map = new ZDC.Map(mapElement, mapOptions, function() {
        console.log('Map initialized successfully');

        // コントロールを追加
        map.addControl(new ZDC.ZoomButton('top-left'));
        map.addControl(new ZDC.Compass('top-right'));
        map.addControl(new ZDC.ScaleBar('bottom-left'));

        const start = new ZDC.LatLng(35.7106, 139.7970); //浅草駅
        const end = new ZDC.LatLng(35.7136, 139.7940); //浅草ROX

        const startMarker = new ZDC.Marker(start);
        const endMarker = new ZDC.Marker(end);
        map.addWidget(startMarker);
        map.addWidget(endMarker);

        const routeUrl = `https://{ドメイン}/route/route_mbn/walk?search_type=4&from=${start.lng},${start.lat}&to=${end.lng},${end.lat}&llunit=dec&datum=JGD`;

        fetch(routeUrl, {
            method: 'GET',
            headers: {
                'x-api-key': '{APIキー}',
                'Authorization': 'ip'
            },
        })
        .then(response => {
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            return response.json();
        })
        .then(data => {
            console.log('API Response:', JSON.stringify(data, null, 2));
            if (data.status === 'OK') {
                if (data.result && data.result.item && data.result.item.length > 0) {
                    const routeItem = data.result.item[0];
                    console.log('Route item:', JSON.stringify(routeItem, null, 2));

                    if (routeItem.route && routeItem.route.section && Array.isArray(routeItem.route.section)) {
                        const sections = routeItem.route.section;
                        console.log('Sections:', JSON.stringify(sections, null, 2));
                        const decodedPath = [];
                        sections.forEach(section => {
                            if (section.link && Array.isArray(section.link)) {
                                section.link.forEach(link => {
                                    if (link.line && link.line.coordinates) {
                                        link.line.coordinates.forEach(coord => {
                                            decodedPath.push(new ZDC.LatLng(coord[1], coord[0]));
                                        });
                                    }
                                });
                            }
                        });

                        if (decodedPath.length > 0) {
                            console.log('Decoded path:', decodedPath);
                            const routeLine = new ZDC.Polyline(decodedPath, {
                                color: '#008dcb',
                                width: 5,
                                opacity: 0.7
                            });
                            map.addWidget(routeLine);
                            console.log('Route line added to map');

                            const bounds = new ZDC.LatLngBounds();
                            decodedPath.forEach(point => bounds.extend(point));
                            map.fitBounds(bounds);
                        } else {
                            console.error('デコードされたパスが空です。sections:', JSON.stringify(sections, null, 2));
                        }
                    } else {
                        console.error('ルートセクションデータが不正です:', routeItem.route);
                    }
                } else {
                    console.error('ルートアイテムが見つかりません:', data.result);
                }
            } else {
                console.error('APIステータスがOKではありません:', data.status);
            }
        })
        .catch(error => {
            console.error('ルート検索エラー:', error);
        });
    }, function() {
        console.error('地図の生成に失敗しました');
    });
});

```
### 実装結果と課題

- **結果**：リクエストパラメータである search_type に 4 を指定することで屋根付き区間を含むルートが地図上に表示されました。
- **課題**：APIレスポンスに含まれる情報が十分であれば、より詳細な屋根付きルートの視覚化が可能になります。

## OpenStreetMapを使用したルート検索

### 実装概要

OSMでは、OSRM（Open Source Routing Machine）を利用して歩行者ルートを検索しました。ただし、OSRMでは屋根付きルートの直接的な検索ができないため、以下のような代替手段を採用しました
- 通常の歩行者ルートを取得
- Overpass APIを使用して、ルート周辺の屋根付き道路（covered=yesタグを持つ道路）を取得
- 取得したルートと屋根付き道路の情報を組み合わせて表示

以下は主な機能です

- **地図の初期化**：Leaflet.jsを使用して地図を表示します。
- **ルート検索**：OSRM APIを使用してルート情報を取得します。
- **屋根付き区間の抽出**：ルート内の特定タグ（例：`covered=yes`）を持つ区間をフィルタリングします。

#### 実装コード

以下は実装したコードです。

```javascript
// マップを初期化する関数
async function initMap() {
    // 地図の中心座標を設定
    const map = L.map('map').setView([35.7106, 139.7970], 15);

    // OSMのタイルレイヤーを追加
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
    }).addTo(map);

    // スタート地点と終点の座標
    const start = [35.7106, 139.7970];
    const end = [35.7136, 139.7940];

    // スタート地点と終点にマーカーを追加
    L.marker(start).addTo(map).bindPopup('スタート地点');
    L.marker(end).addTo(map).bindPopup('終点');

    // ルート検索のAPIリクエストURL
    const url = `https://router.project-osrm.org/route/v1/foot/${start[1]},${start[0]};${end[1]},${end[0]}?overview=full&geometries=geojson`;

    try {
        const response = await fetch(url);
        const data = await response.json();

        // OSRMから取得したルートデータ
        const originalRoute = data.routes[0];

        // ルートの範囲を取得
        const bounds = getBoundsFromRoute(originalRoute);

        // Overpass API で屋根付き道路を取得
        const coveredWays = await getCoveredWays(bounds);

        // ルートをフィルタリング
        const filteredRoute = filterCoveredRoute(originalRoute, coveredWays);

        // フィルタリング後のルートを地図上に描画
        const coordinates = filteredRoute.geometry.coordinates;

        // 座標の順序を入れ替え（OSRM APIは経度,緯度の順で返すため）
        const latLngs = coordinates.map(coord => [coord[1], coord[0]]);

        // ルートを地図上に描画
        L.polyline(latLngs, { color: 'blue' }).addTo(map);

        // ルート全体が表示されるようにマップの表示範囲を調整
        map.fitBounds(L.latLngBounds(latLngs));
    } catch (error) {
        console.error('Error:', error);
    }
}

// ルートの範囲を取得する関数
function getBoundsFromRoute(route) {
    const coordinates = route.geometry.coordinates;
    return {
        south: Math.min(...coordinates.map(coord => coord[1])),
        west: Math.min(...coordinates.map(coord => coord[0])),
        north: Math.max(...coordinates.map(coord => coord[1])),
        east: Math.max(...coordinates.map(coord => coord[0])),
    };
}

// Overpass API を使用して屋根付き道路を取得
async function getCoveredWays(bounds) {
    const overpassQuery = `
        [out:json];
        (
            way["covered"="yes"](${bounds.south},${bounds.west},${bounds.north},${bounds.east});
            way["tunnel"="building_passage"](${bounds.south},${bounds.west},${bounds.north},${bounds.east});
        );
        out body;
    `;
    const overpassUrl = `https://overpass-api.de/api/interpreter?data=${encodeURIComponent(overpassQuery)}`;

    try {
        const response = await fetch(overpassUrl);
        const data = await response.json();
        return data.elements.filter(element => element.type === "way");
    } catch (error) {
        console.error("Overpass API error:", error);
        return [];
    }
}

// ルートをフィルタリングして、屋根付き道路を優先
function filterCoveredRoute(route, coveredWays) {
    const coordinates = route.geometry.coordinates;

    // 取得した coveredWays に該当するルートのみを選択
    const filteredCoordinates = coordinates.filter(coord =>
        coveredWays.some(way =>
            way.nodes.some(node => Math.abs(node.lat - coord[1]) < 0.0001 && Math.abs(node.lon - coord[0]) < 0.0001)
        )
    );

    // 屋根付きルートがあればそれを優先
    if (filteredCoordinates.length > 0) {
        console.log("屋根付きルートを適用");
        return { geometry: { coordinates: filteredCoordinates } };
    } else {
        console.log("通常ルートを適用");
        return route;
    }
}

```
### 実装結果と課題

- **結果**：通常の歩行者ルートと、周辺の屋根付き道路を地図上に表示することができました。
- **課題**：OSRM APIでは屋根付きルートの直接的な検索ができないため、完全な屋根付きルートの抽出は困難です。また、OSMのデータの精度や完全性に依存するため、全ての屋根付き道路が正確に反映されるわけではありません。

## 地図表示
- **ZENRIN Maps API**
![zma_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3979666/b410af58-596c-4a3e-84fb-3fb65e0bf81c.png)

- **OSM**
![oms_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3979666/a3c370b2-3a25-4a05-a3a7-60ac2bdb96aa.png)

## API比較
### 条件付き歩行者ルート実装の容易さ
ZENRIN Maps APIでは、APIリクエストを送るだけで屋根付きルートを考慮したルートを取得できるため、開発者の負担が非常に少なくなります。これにより、迅速かつ効率的な実装が可能となります。
一方、OSMを利用する場合は、より複雑な手順が必要となります。まず、OSRM（Open Source Routing Machine）などのルート検索エンジンをセットアップする必要があります。さらに、タグ情報をフィルタリングする処理を追加することでカスタマイズの自由度が高くなります。しかし、この高い自由度は同時に開発の負担増加につながることに注意が必要です。

### OSRMの自己ホスティングと屋根優先設定
OSRMの標準プロファイルでは「屋根付き」という属性を直接扱うことはできません。
そのため、カスタムプロファイルを作成する必要があります。カスタムプロファイルを使用することで、「covered=yes」タグに基づいた優先度設定が可能になります。
これにより、屋根付きの道路を優先したルート検索を実現することができます。
以下に手順を記載します。
今回四国エリアのデータを対象に屋根付き道路を優先するルート検索を確認したところ、屋根のある道を通らないケースも一部見受けられました。タグが適切に設定されていないエリアがありそうです。

##### 手順 1: Dockerのインストール
- OSRMバックエンド用のDockerイメージを取得します。
##### 手順 2: データの準備
- [OSRMのバックエンドに関連するソースコード](https://github.com/Project-OSRM/osrm-backend)をローカルにダウンロード
- [Geofabrik](https://download.geofabrik.de/)から対象エリアのOSMデータをローカルにダウンロード
- プロファイルファイルの作成（以下に foot.lua を記載）
```bash
api_version = 2

Set = require('lib/set')
Sequence = require('lib/sequence')
Handlers = require("lib/way_handlers")
find_access_tag = require("lib/access").find_access_tag

function setup()
  local walking_speed = 5
  local covered_speed_bonus = 1.2  -- 屋根のある道の速度ボーナス
  local covered_weight_factor = 0.8  -- 屋根のある道の重み係数

  return {
    properties = {
      weight_name                   = 'duration',
      max_speed_for_map_matching    = 40/3.6,
      call_tagless_node_function    = false,
      traffic_light_penalty         = 2,
      u_turn_penalty                = 2,
      continue_straight_at_waypoint = false,
      use_turn_restrictions         = false,
    },

    default_mode            = mode.walking,
    default_speed           = walking_speed,
    oneway_handling         = 'specific',

    barrier_blacklist = Set { 'yes', 'wall', 'fence' },

    access_tag_whitelist = Set { 'yes', 'foot', 'permissive', 'designated' },

    access_tag_blacklist = Set { 'no', 'agricultural', 'forestry', 'private', 'delivery' },

    restricted_access_tag_list = Set { },

    restricted_highway_whitelist = Set { },

    construction_whitelist = Set {},

    access_tags_hierarchy = Sequence { 'foot', 'access' },

    service_access_tag_blacklist = Set { },

    restrictions = Sequence { 'foot' },

    suffix_list = Set { 'N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'North', 'South', 'West', 'East' },

    avoid = Set { 'impassable', 'proposed' },

    speeds = Sequence {
      highway = {
        primary = walking_speed, primary_link = walking_speed,
        secondary = walking_speed, secondary_link = walking_speed,
        tertiary = walking_speed, tertiary_link = walking_speed,
        unclassified = walking_speed, residential = walking_speed,
        road = walking_speed, living_street = walking_speed,
        service = walking_speed, track = walking_speed,
        path = walking_speed, steps = walking_speed,
        pedestrian = walking_speed, platform = walking_speed,
        footway = walking_speed, pier = walking_speed,
      },
      railway = { platform = walking_speed },
      amenity = { parking = walking_speed, parking_entrance = walking_speed },
      man_made = { pier = walking_speed },
      leisure = { track = walking_speed }
    },

    route_speeds = { ferry = 5 },

    bridge_speeds = {},

    surface_speeds = {
      fine_gravel = walking_speed * 0.75,
      gravel = walking_speed * 0.75,
      pebblestone = walking_speed * 0.75,
      mud = walking_speed * 0.5,
      sand = walking_speed * 0.5
    },

    tracktype_speeds = {},

    smoothness_speeds = {},

    -- 新しい変数を追加
    covered_speed_bonus = covered_speed_bonus,
    covered_weight_factor = covered_weight_factor
  }
end

function process_node(profile, node, result)
  local access = find_access_tag(node, profile.access_tags_hierarchy)
  if access and profile.access_tag_blacklist[access] then
    result.barrier = true
  else
    local barrier = node:get_value_by_key("barrier")
    if barrier then
      local bollard = node:get_value_by_key("bollard")
      local rising_bollard = bollard and "rising" == bollard
      if profile.barrier_blacklist[barrier] and not rising_bollard then
        result.barrier = true
      end
    end
  end

  local highway = node:get_value_by_key("highway")
  if "traffic_signals" == highway then
    result.traffic_lights = true
  end
end

function process_way(profile, way, result)
  local data = {
    highway = way:get_value_by_key('highway'),
    bridge = way:get_value_by_key('bridge'),
    route = way:get_value_by_key('route'),
    leisure = way:get_value_by_key('leisure'),
    man_made = way:get_value_by_key('man_made'),
    railway = way:get_value_by_key('railway'),
    platform = way:get_value_by_key('platform'),
    amenity = way:get_value_by_key('amenity'),
    public_transport = way:get_value_by_key('public_transport'),
    covered = way:get_value_by_key('covered'),
    tunnel = way:get_value_by_key('tunnel')
  }

  if next(data) == nil then
    return
  end

  local handlers = Sequence {
    WayHandlers.default_mode,
    WayHandlers.blocked_ways,
    WayHandlers.access,
    WayHandlers.oneway,
    WayHandlers.destinations,
    WayHandlers.ferries,
    WayHandlers.movables,
    WayHandlers.speed,
    WayHandlers.surface,
    WayHandlers.classification,
    WayHandlers.roundabouts,
    WayHandlers.startpoint,
    WayHandlers.names,
    WayHandlers.weights
  }

  WayHandlers.run(profile, way, result, data, handlers)

  -- 屋根のある道路や通路を優先
  if data.covered == "yes" or data.tunnel == "building_passage" then
    result.forward_speed = result.forward_speed * profile.covered_speed_bonus
    result.backward_speed = result.backward_speed * profile.covered_speed_bonus
    result.forward_rate = result.forward_rate / profile.covered_weight_factor
    result.backward_rate = result.backward_rate / profile.covered_weight_factor
  end
end

function process_turn(profile, turn)
  turn.duration = 0.0
  if turn.direction_modifier == direction_modifier.u_turn then
    turn.duration = turn.duration + profile.properties.u_turn_penalty
  end
  if turn.has_traffic_light then
    turn.duration = turn.duration + profile.properties.traffic_light_penalty
  end
  if profile.properties.weight_name == 'routability' then
    if not turn.source_restricted and turn.target_restricted then
      turn.weight = turn.weight + 3000
    end
  end
end

return {
  setup = setup,
  process_way = process_way,
  process_node = process_node,
  process_turn = process_turn
}
```
##### 手順 3: OSRMデータの処理
- OSMデータをルーティング可能な形式に変換
##### 手順 4: 検索実行
- OSRMサーバーの起動し、ブラウザ上でリクエスト打鍵またはルート検索の地図描画を実施


## まとめ
今回の比較では、ZENRIN Maps APIの方が簡単に実装できるため、開発の手間を省きたい場合には適しています。
一方、OSMは無料でカスタマイズ性が高いですが、屋根付きルートの実装には技術的な知識と時間が必要で開発の負担は大きい印象です。
プロジェクトの要件、技術的な制約や開発にかけられる時間を考慮して適切なAPIを選択が必要です。