ストリートマップを使って道順を確認する機能をつくる

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、以前このブログでも取り上げた地図ライブラリ「Leaflet」ですが、近年はOpenStreetMapの発展もあって、お店の情報も細かく記載されるようになりました。

結果として、精度を求めないなら(条件によっては有料の)Googleマップを使う必要はなくなってると感じています。

ただ、そうはいっても「Googleマップ」には強力な武器が1つあります。

それは・・・・・・

Google ストリートビュー

です。

皆さんも一度はご覧になったことがあると思いますが、「あたかもその場所にいるかのように」360度を見回すことができるパノラマ写真サービスのことですね。

※ 実はストリートビューの代替サービスとして以下のようなサービスも存在してはいるのですが、質としてはやはりGoogleには勝ててないというのが印象です。

そして、そんなストリートビューが役立つ場面というのが・・・・・・

土地勘がない場所の道順をチェックする場合

です。

つまり、事前にブラウザ内で「現地へ行って」おいて、どこをどっちに曲がるのかを確認できるということですね。

そこで❗

今回は、「Leaflet + Google ストリートビュー」を使って、地図上をクリックした道順をストリートビューで確認できる機能をつくってみたいと思います。

ぜひ何かの参考になりましたら嬉しいです。😊✨

「時計をして出かけたら、
変な日焼けになってしまいました…💦」

開発環境: Vue 3

前提として

ストリートビューの埋め込み表示をするには、Google Cloudの「Maps Embed API」が実行できるAPIキーが必要になります。

もしまだの方は以下の記事を参考にして事前に取得しておいてください。

📝 参考ページ: Google Embed Map の APIキーを取得する

やりたいこと

今回やりたいことは以下のとおりです。

  • 地図上で道順をクリックできる
  • クリックした場所のストリートビューを表示
  • 次の位置との角度を計算し、ストリートビューをその方向へ向ける

なお、今回は「大阪の難波駅 → なんばグランド花月へ行く」という想定にしていますので、初期位置は難波駅付近にします。(お好みで変更してください)

では実際にやってみましょう❗

道順をストリートビューで表示するコード

では、今回もクライアント・サイドのみで実装できますので、いきなりコードのご紹介です。

multiple_street_view.html

<html>
<head>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://unpkg.com/leaflet@1.8.0/dist/leaflet.css" rel="stylesheet">
    <style>

        #map, #street_view {

            width: 100%;
            height: 400px;
            border: 1px solid #777;

        }

    </style>
</head>
<body>
    <div id="app" class="p-5">
        <h3 class="mb-4">&#x1F5FA; ストリートマップを使った道順を表示するサンプル</h3>
        <div class="row">
            <div class="col-6">
                <div id="map"></div>
            </div>
            <div class="col-6">
                <div v-if="hasLocation">
                    <iframe
                        :src="streetViewUrl"
                        id="street_view"
                        class="mb-3"
                        allowfullscreen=""
                        loading="lazy"
                        referrerpolicy="no-referrer-when-downgrade"></iframe>
                    <pagination
                        :page="page"
                        :locations="locations"
                        @move-page="onMovePage"></pagination>
                </div>
            </div>
        </div>
    </div>
    <script src="https://unpkg.com/leaflet@1.8.0/dist/leaflet.js"></script>
    <script src="https://unpkg.com/vue@3.2.31/dist/vue.global.prod.js"></script>
    <script>

        const { createApp, ref, computed, onMounted, defineComponent } = Vue;
        const GOOGLE_MAP_API_KEY = '(ここにあなたの API キー)';

        // Component
        const paginationComponent = {
            template: `
                <div>
                    <button type="button" class="btn btn-primary me-2" :disabled="!hasPrev" @click="movePage('prev')">前へ</button>
                    <span v-text="page"></span> / <span v-text="locations.length"></span>
                    <button type="button" class="btn btn-primary ms-2" :disabled="!hasNext" @click="movePage('next')">次へ</button>
                </div>
            `,
            props: {
                page: {
                    type: Number,
                    required: true,
                },
                locations: {
                    type: Array,
                    required: true,
                },
            },
            emits: ['movePage'],
            setup(props, { emit }) {

                // Event
                const movePage = direction => {

                    const additionalPages = {
                        prev: -1,
                        next: 1,
                    };
                    const additionalPage = additionalPages[direction] || 0;
                    const page = props.page + additionalPage;

                    emit('movePage', { page }); // イベント送出(@move-page が実行される)

                };

                // Pagination
                const hasPrev = computed(() => {

                    return props.page > 1;

                });
                const hasNext = computed(() => {

                    const maxPage = props.locations.length;
                    return props.page < maxPage;

                });

                return {
                    movePage,
                    hasPrev,
                    hasNext,
                };

            }
        };

        createApp({
            setup() {

                // Data
                const page = ref(1);
                const locations = ref([]);
                let map = null;

                // Computed
                const hasLocation = computed(() => {

                    return locations.value.length > 0;

                });
                const streetViewUrl = computed(() => {

                    if(hasLocation.value === true) {

                        const index = page.value - 1;
                        const currentLocation = locations.value[index];
                        const { latitude, longitude } = currentLocation;
                        const hasNextPage = locations.value.length > page.value;
                        let heading = 0;

                        if(hasNextPage === true) { // 向いている方向を計算

                            const nextLocation = locations.value[index+1];
                            const dx = nextLocation.longitude - currentLocation.longitude;
                            const dy = nextLocation.latitude - currentLocation.latitude;
                            heading = 90 - Math.atan2(dy, dx) * 180 / Math.PI;

                        }

                        return `https://www.google.com/maps/embed/v1/streetview`+
                            `?key=${GOOGLE_MAP_API_KEY}&location=${latitude},${longitude}&heading=${heading}`;

                    }

                    return '';

                });

                // Methods
                const onMapClick = e => {

                    const latitude = e.latlng.lat;
                    const longitude = e.latlng.lng;

                    locations.value.push({
                        latitude,
                        longitude
                    });

                    L.marker([latitude, longitude]).addTo(map);

                };
                const onMovePage = e => {

                    page.value = e.page;

                };

                // Mounted
                onMounted(() => {

                    const defaultLocation = [34.66449642610138, 135.50271570682528]; // 大阪・難波駅の周辺
                    const zoomLevel = 17;
                    map = L.map('map').setView(defaultLocation, zoomLevel);

                    const tileLayerUrl = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
                    const tileLayerAttribution = '<a target="_blank" href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
                    const layer = L.tileLayer(tileLayerUrl, { attribute: tileLayerAttribution });

                    map.addLayer(layer);
                    map.on('click', onMapClick); // クリックイベント

                });

                return {
                    page,
                    locations,
                    hasLocation,
                    streetViewUrl,
                    onMovePage,
                }

            }
        })
        .component('pagination', paginationComponent)
        .mount('#app');

    </script>
</body>
</html>

まず、コード内にある「(ここにあなたの API キー)」という部分はGoogle Cloudで取得したAPIキーに入れ替えてください。

そして、このコード中で少し難しいのは、「向いている方向を計算」している部分です。

私も数学は苦手なので、計算式はググってきて適用したのですが、それだけではうまくいきませんでした。

なぜなら、ストリートビューの基準点は北(上)だからです。
逆に数学の角度は東(右)が基準のため、90度の修正が必要になりました。

今回は簡単ですが、作業は終了です。
お疲れ様でした😊✨

テストしてみる

では、実際にテストしてみましょう❗

※ なお、ここで紹介するスクリーンショットは著作権の問題があるため、ぼかしをかけています。もし分かりにくい場合は、デモページをご覧ください。

まずはブラウザでアクセスします。

すると、以下のように地図が表示されます。

では、「大阪の難波駅 → なんばグランド花月」の道順をクリックしていきましょう。

すると・・・・・・

はい❗
クリックした位置にマーカーが表示され、(切れていますが)右側にストリートビューが表示されました。

しかも(ぼかしで分かりにくいですが)なんばグランド花月に向かう方向になっています。

では、「次へ」ボタンをクリックして移動できるかチェックしてみましょう。

どうなったでしょうか・・・・・・???

はい❗

場所が移動しました。今回も正しい方向を向いているため、遠くに「道具屋筋商店街」が見えていました。(再度分かりにくくてすみません…💦)

そして、最終的にはなんばグランド花月へ到着です。

全て成功です😊✨

デモページをつくりました

さすがに、ぼかし付きではわかりにくかったので、デモページをつくりました。ぜひ実際に触って試してみてください👍

📝 デモページ

企業様へのご提案

ストリートビューを利用することで、配送やルート営業の道順を事前に確認することができ、フタッフさんたちがスムーズに業務を進めることができるでしょう。

また、LeafletOpenStreetMap)はもちろんですが、今回のような埋込み型のストリートビューは無料で利用できます。

つまり、今回のコードを実行しても料金はかからないため、固定コストが不要です。

もしこういった機能をご希望でしたら、お気軽にお問い合わせからご連絡ください。

どうぞよろしくお願いいたします。m(_ _)m

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回は道順をストリートビューで確認する機能を実装してみました。

ちなみに、今回もVue 3を使っていますが、ネット上でもいろいろと言われているとおり「やっぱりちょっと使いにくいかも…」という気持ちになりました。(私は今でもVue 2派です👍)

*****.valueは冗長ですし、propsもクセがあります。

そろそろ時代の流れということで、私もVue → Reactへ移行するときがきたのかもしれません。Viteが標準搭載になったこともひとつの大きな要因ですね。

ちょっと、自分の中で重要会議をやってみることにします(笑)

ではでは〜❗

「Vue 4とかはまだ来ないのかな??」

このエントリーをはてなブックマークに追加       follow us in feedly