ストーリーマップとは
ストーリーマップとは、文章(ストーリー)などのコンテンツと連動して地図が動くタイプのウェブマップです。通常のウェブマップは地図自体を自分で動かしますが、ストーリーマップは文章などの場面に応じて地図が自動で動くように制御します。
ストーリーマップの事例
例としては「地理院地図Vector(仮称)のベクトルタイルを使って地図語りを試した(UNVTとMapLibreを利用)」
やそのデモ(外部リンク)、あとArcGISのESRIが力を入れています。(事例集のサイト)。また、地図ライブラリMapLibreの公式サイト(外部リンク)でも同様のサンプルが示されています。
新聞社の報道特集サイトでもこの手法を採用しているケースがあった気がします。
ポップアップ駆動のストーリーマップの提案
ストーリーマップの基本形は、「文章などのコンテンツをスクロールするとその位置に応じてマップの表示位置やズームレベルが変化する」という挙動になっています。
一方で、内容や見せたいものによってはスクロールよりもポップアップでコンテンツを見せるほうが視線誘導の観点で良いケースもあるのでは?ということで、今回ポップアップ駆動タイプのストーリーマップを作ってみました。
デモはこちら(外部リンク)から確認いただけます。
サンプルコード
MapLibre GL JSをベースに作成しています。デモのソースコードはこちらのGithubに置いてありますので、以下にはメイン箇所のサンプルコードを載せます。
//モジュールやスタイルシートのインポート
import * as maplibregl from "maplibre-gl";
import 'maplibre-gl/dist/maplibre-gl.css';
import './style.css';
//初期表示の設定
const init_center = [139.93003, 35.72164-1]; //地図の中心座標
const init_coord = [139.93003, 35.72164]; //ポップアップの位置座標
const init_zoom = 5;
const init_bearing = 0;
const init_pitch = 0;
//ベースマップの設定
const map = new maplibregl.Map({
    container: 'map',
    style:'https://tile2.openstreetmap.jp/styles/osm-bright-ja/style.json', //Many thanks to smellman & Sakura Internet for hosting the osm planet vector-tile.
    center: init_center,
    interactive: true, //一応、手動でも地図を動かせるようにしている
    zoom: init_zoom,
    bearing: init_bearing,
    pitch: init_pitch
});
//文章(ストーリー)をチャプターごとに記述
const chapters = {
    '1. Chiba, Japan': {
        center: init_center,
        zoom: init_zoom,
        bearing: init_bearing,
        pitch: init_pitch,
        coordinates: init_coord,
        caption: '<p>Contents for the 1st chapter.</p>',
        popup_flag: 'top',
    },
    '2. Kandy, Sri Lanka': {
        center: [80.69039, 7.25143-1.5],
        zoom: 6,
        bearing: 0,
        pitch: 0,
        speed: 0.5,
        coordinates: [80.69039, 7.25143],
        caption: '<p>Contents for the 2nd chapter.</p>',
        popup_flag: 'top',
    },
    '3. Bucharest, Romania': {
        center: [26.10317, 44.43653-0.5],
        zoom: 7,
        bearing: 0,
        pitch: 0,
        speed: 0.5,
        coordinates: [26.10317, 44.43653],
        caption: '<p>Contents for the 3rd chapter.</p>',
        popup_flag: 'top',
    },
    '4. Tokyo, Japan': {
        center: [139.73872, 35.71625-1],
        zoom: 5,
        bearing: 0,
        pitch: 0,
        speed: 0.5,
        coordinates: [139.73872, 35.71625],
        caption: '<p>Contents for the last chapter.</p>',
        popup_flag: 'top',
    },
}
//ポップアップを上向き・下向きのどちらに設置するかチャプターごとに指定するためのフラグ
let popup_top = new maplibregl.Popup({closeButton: true, closeOnClick: false, focusAfterOpen: false, anchor:"top", className:"t-popup", maxWidth:'320px'});
let popup_btm = new maplibregl.Popup({closeButton: true, closeOnClick: false, focusAfterOpen: false, anchor:"bottom", className:"t-popup", maxWidth:'320px'});
//チャプター名をリストに格納
const chapterNames = Object.keys(chapters);
for (let i = 0; i < chapterNames.length; i++) {
    const selectChapter = document.getElementById('chapter-id');
    const optionName = document.createElement('option');
    optionName.value = chapterNames[i];
    optionName.textContent = chapterNames[i];
    selectChapter.appendChild(optionName);
}
//最初のポップアップを設置
if (chapters[chapterNames[0]]["popup_flag"]==='top'){
    popup_top.setLngLat(chapters[chapterNames[0]]["coordinates"]).setHTML(chapters[chapterNames[0]]["caption"]).setOffset([0,10]).addTo(map);
}else{
    popup_btm.setLngLat(chapters[chapterNames[0]]["coordinates"]).setHTML(chapters[chapterNames[0]]["caption"]).setOffset([0,-10]).addTo(map);
};
//セレクタ変更時に挙動を分岐するためのフラグ
let select_flag = 0;
//チャプターのカウント用変数
let chapter_i = 1;
//チャプター変更時の挙動
const changePopup = function changePopup() {
    select_flag = 1;
    popup_top.remove();
    popup_btm.remove();
    if (chapter_i < chapterNames.length) {
        let chapter_id = chapterNames[chapter_i];
        map.flyTo({
            center: chapters[chapter_id]["center"],
            zoom: chapters[chapter_id]["zoom"],
            bearing: chapters[chapter_id]["bearing"],
            pitch: chapters[chapter_id]["pitch"],
            speed: chapters[chapter_id]["speed"],
        });
        if (chapters[chapter_id]["popup_flag"]==='top'){
            popup_top.setLngLat(chapters[chapter_id]["coordinates"]).setHTML(chapters[chapter_id]["caption"]).setOffset([0,10]).addTo(map);
        }else{
            popup_btm.setLngLat(chapters[chapter_id]["coordinates"]).setHTML(chapters[chapter_id]["caption"]).setOffset([0,-10]).addTo(map);
        };
        let trigger = document.getElementsByClassName("t-popup");
        triggers = Array.from(trigger);
        triggers.forEach(function(target) {
            target.addEventListener('click', changePopup);
        });
        const select = document.getElementById('chapter-id');        
	    select.options[chapter_i].selected = true;
        chapter_i++;
    }else {
        popup_top.setLngLat(init_coord).setHTML('<p>Thank you!</p>').addTo(map);
    };
    select_flag = 0;
}
window.changePopup = changePopup;
//チャプターを手動でも選択できるようにセレクタを設定
const selectedChapter = document.querySelector('.chapter-set');
selectedChapter.addEventListener('change', function () {
    if (select_flag === 0){
        chapter_i = selectedChapter.selectedIndex;
        changePopup();
    }else{
        chapter_i = selectedChapter.selectedIndex;
        select_flag = 0;
    };
});
//チャプターを変更するためのトリガー設定(今回はポップアップ全体を指定)
let trigger = document.getElementsByClassName("t-popup");
let triggers = Array.from(trigger);
triggers.forEach(function(target) {
  target.addEventListener('click', function () {
      changePopup();
  });
});
備考
手探りな部分もありますが、作ってみて感じたことを挙げておきます。
- 自身の経歴や旅行の思い出を時系列的に表現するのに使えそう。
- ユーザーは視点を画面中心から動かさずに見ることができるので、閲覧時のストレスが少ない気がする。
- スマホでも快適に見れるように、地図の中心やポップアップの座標値・表示方法、ズームレベル等の設定はチャプターごとに細かく調整する必要がある。
- セレクトボックスを別途用途して、任意のチャプターに自由に飛べるようにしておく。
- 他のレイヤ(マーカーやエリア境界など)を用意しておいて、チャプターごとに必要に応じて表示・非表示にすると更にリッチなコンテンツになる。
以上です。
まだ改善の余地は大きいと思いますが、ナレッジの一つになれば幸いです。
