警察庁の交通事故統計情報のオープンデータをCARTO VLでアニメーション化してみました

# はじめに
- [警察庁の交通事故統計情報のオープンデータ](https://www.npa.go.jp/publications/statistics/koutsuu/opendata/index_opendata.html)を[CARTO VL](https://carto.com/developers/carto-vl/)でアニメーション化してみました。
- CARTO VLは、時系列データのアニメーション化等ができる、オープンソースのJavaScriptライブラリです。
- 今回用いている[CARTO VL](https://carto.com/developers/carto-vl/guides/animated-visualizations/)のバージョンはv1.4.4です。
- 警察庁オープンデータは、2019年の本票を用いています。

# アウトプットイメージ
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">警察庁オープンデータ（2019年、38万件）をCARTO VL（<a href="https://t.co/55wQvvJnzZ">https://t.co/55wQvvJnzZ</a>）というライブラリーで時系列で表示してみた。青：負傷事故、赤：死亡事故。時系列データのアニメーションがさくっとできて良い📝<a href="https://twitter.com/hashtag/CARTOVL?src=hash&amp;ref_src=twsrc%5Etfw">#CARTOVL</a> <a href="https://twitter.com/hashtag/Mapbox?src=hash&amp;ref_src=twsrc%5Etfw">#Mapbox</a><br>【警察庁】交通事故統計情報のオープンデータ<a href="https://t.co/YsIpol9K2Z">https://t.co/YsIpol9K2Z</a> <a href="https://t.co/toMaO8fELe">https://t.co/toMaO8fELe</a> <a href="https://t.co/MJh585koJY">pic.twitter.com/MJh585koJY</a></p>&mdash; shi_works🌥️ (@syanseto) <a href="https://twitter.com/syanseto/status/1517345288570097664?ref_src=twsrc%5Etfw">April 22, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 前提条件
- アニメーション化で用いる、警察庁オープンデータ（2019年・本票）は、CSV形式のデータになっていますので、これを[QGIS](https://qgis.org/ja/site/forusers/download.html)等でgeojson形式のデータに変換しています。
- 変換後のgeojson形式のデータは、以下のとおりです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205045/1edbb7ce-a1eb-cbc9-c7a1-af7f240359eb.png)
- 警察庁オープンデータ（2019年・本票）は、以下のとおり加工しています。
- 「発⽣⽇時 年」、「発⽣⽇時 ⽉」、「発⽣⽇時 ⽇」、「発⽣⽇時 時」、「発⽣⽇時 分」をもとに、「date_time」を作成。
- 「事故内容」をもとに、「jiko_details」を作成（1=死亡、2=負傷）。
- 「地点 緯度（北緯）」、「地点 経度（東経）」をもとに、「coordinates」を作成。
- また、[国土数値情報の高速道路時系列データ（geojson形式）](https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-N06-v1_2.html)を用いています。
- 背景地図には、[Mapbox](https://www.mapbox.com/)の地図を用いているため、別途、アクセストークンの取得が必要です。

# html
```html:Npa_Animation.html
<!DOCTYPE html>
<html>

<head>
    <title>Npa_Animation | CARTO VL</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta charset="UTF-8">
    <script src="https://libs.cartocdn.com/carto-vl/v1.4.4/carto-vl.min.js"></script>
    <script src='https://api.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.js'></script>
    <link href='https://api.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.css' rel='stylesheet' />
    <link href="https://fonts.googleapis.com/css?family=Roboto:100,200,400,500" rel="stylesheet">
    <link href="https://carto.com/developers/carto-vl/v1.4.4/examples/maps/style.css" rel="stylesheet">
    <link rel="stylesheet" href="style.css" />
</head>

<body>
    <!-- Map goes here -->
    <div id="map"></div>

    <!-- Animation control elements -->
    <aside class="toolbox">
        <div class="box">
            <header>
                <h1>Animation controls</h1>
            </header>
            <section style="margin-top: 15px;">
                <input type="button" id="js-play-button" disabled>
                <input type="button" id="js-pause-button">
                <input type="range" id="js-time-range" min="0" max="1" step="0.01">
            </section>
            <br />
            <section>
                <span id="js-current-time" class="open-sans"></span>
            </section>
            <hr>
            <section>
                <span style="margin-right: 5px" class="open-sans">Duration (seconds)</span>
                <input class="white-thumb" type="range" id="js-duration-range" min="1" max="120" step="1">
                <span style="margin-left: 5px" class="open-sans" id="js-duration-span">30</span>
            </section>
        </div>
    </aside>
    <script src="script.js"></script>
</body>

</html>
```
# css
```css:style.css
aside.toolbox {
  right: 96px;
}

.box {
  width: 320px;
  background: #f2f2f2;
}

section {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  align-items: center;
}

hr {
  margin: 15px auto;
}

input[type=range] {
  -webkit-appearance: none;
  border: 1px solid white;
  background: transparent;
  border: none;

  cursor: pointer;
  flex: 1;
  padding: auto 10px;

  margin: auto 5px;
}

input[type=range]::-webkit-slider-runnable-track {
  height: 3px;
  background: #1785FB;
  border: none;
  border-radius: 3px;
}

input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none;
  border: none;
  height: 10px;
  width: 10px;
  border-radius: 50%;
  background: #1785FB;
  margin-top: -4px;
}

input[type=range]:focus {
  outline: none;
}

input[type=range]::-moz-range-track {
  height: 3px;
  background: #1785FB;
  border: none;
  border-radius: 3px;
}

input[type=range]::-moz-range-thumb {
  border: none;
  height: 10px;
  width: 10px;
  border-radius: 50%;
  background: #1785FB;
}

input[type=range].white-thumb::-moz-range-thumb {
  border-radius: 50%;
  border: 2px solid #1785FB;
  background: white;
  height: 12px;
  width: 12px;
}

input[type=range].white-thumb::-webkit-slider-thumb {
  border-radius: 50%;
  border: 3px solid #1785FB;
  background: white;
  height: 15px;
  width: 15px;
  margin-top: -6px;
}

input[type=range].white-thumb::-ms-thumb {
  border-radius: 50%;
  border: 2px solid #1785FB;
  background: white;
  height: 12px;
  width: 12px;
}

input[type=range]:-moz-focusring {
  outline: 1px solid white;
  outline-offset: -1px;
}

input[type=range]::-ms-track {
  height: 3px;
  background: transparent;
  border-color: transparent;
  border-width: 6px 0;
  color: transparent;
}

input[type=range]::-ms-fill-lower {
  background: #1785FB;
}

input[type="range"]::-moz-range-progress {
  background: #1785FB;
}

input[type=range]::-ms-fill-upper {
  background: #ccc;
}

input[type="range"]::-moz-range-track {
  background: #ccc;
}

input[type=range]::-ms-thumb {
  border: none;
  height: 10px;
  width: 10px;
  border-radius: 50%;
  background: #1785FB;
}

input[type="button"] {
  width: 36px;
  height: 36px;
  border-radius: 4px;
  border: 0;
  box-shadow: none;
  color: #fff;
  cursor: pointer;
  display: inline-flex;
  font: 500 12px/20px 'Roboto';
  margin: 0;
  margin-right: 10px;
}

#map {
  flex: 1;
}

#js-duration-span {
  background: white;
  border: 1px solid #ddd;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
}

#js-play-button {
  background: url('./play.svg') no-repeat;
  background-color: #1785FB;
  background-position: center;
}

#js-pause-button {
  background: url('./pause.svg') no-repeat;
  background-color: #1785FB;
  background-position: center;
}
```
# javascript
```javascript:script.js
// Add basemap and set properties
mapboxgl.accessToken = 'Mapboxのアクセストークンを入力してください'
const map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/dark-v10',
    center: [139.7500609, 35.686168],
    zoom: 10,
    scrollZoom: true
});

const nav = new mapboxgl.NavigationControl();
map.addControl(nav, 'top-left');
map.addControl(new mapboxgl.FullscreenControl(), 'top-left');

//** CARTO VL functionality begins here **//

// Autenticate the client
carto.setDefaultAuth({
    username: 'cartovl',
    apiKey: 'default_public'
});

// Define source
fetch('警察庁オープンデータ（geojson形式）のURLを入力してください')
    .then(response => response.json())
    .then(function (data) {
        // Define layer
        const dateColumns = ['date_time'];
        const jikoColumns = ['jiko_details'];
        const options = { dateColumns, jikoColumns };
        const source = new carto.source.GeoJSON(data, options);
        const viz = new carto.Viz(`
            @duration: 60
            @animation: animation(@timeSteps, @duration, fade(0, 1))
            @timeSteps: linear($date_time, time('2019-01-01T00:00:00'), time('2019-12-31T23:59:59'))
            color: rgba(255, 255, 255, 0.8)
            strokeColor: ramp(buckets($jiko_details, [2, 3]), [rgba(255, 0, 255, 1.0), rgba(0, 255, 255, 0.8), rgba(0, 255, 255, 0.8)])
            filter: @animation
            width: 6
            strokeWidth: 1.5
        `);

        // Define map layer
        const layer = new carto.Layer('layer', source, viz);

        // Add map layer
        layer.addTo(map);

        // Get HTML elements
        const $playbutton = document.getElementById('js-play-button');
        const $pausebutton = document.getElementById('js-pause-button');
        const $durationRange = document.getElementById('js-duration-range');
        const $timeRange = document.getElementById('js-time-range');
        const $spanDuration = document.getElementById('js-duration-span');
        const $currentTime = document.getElementById('js-current-time');

        // Listen to layer events
        let last = $timeRange.value;

        layer.on('updated', () => {
            if ($timeRange.value == last) {
                $timeRange.value = viz.variables.animation.getProgressPct();
                last = $timeRange.value;
            }
            $currentTime.innerText = viz.variables.animation.getProgressValue().toString();
        });

        $timeRange.addEventListener('change', () => {
            // Update animation progress
            viz.variables.animation.setProgressPct($timeRange.value);
            last = $timeRange.value;
        });

        // Listen to interaction events
        $durationRange.addEventListener('change', () => {
            const duration = parseInt($durationRange.value, 10);
            // Update animation duration
            viz.variables.duration = $spanDuration.innerHTML = duration;
        });

        $playbutton.addEventListener('click', () => {
            // Play the animation
            viz.variables.animation.play()
            $playbutton.disabled = true;
            $pausebutton.disabled = false;
            console.log('Playing: ', viz.variables.animation.isPlaying());
        });

        $pausebutton.addEventListener('click', () => {
            // Pause the animation
            viz.variables.animation.pause();
            $playbutton.disabled = false;
            $pausebutton.disabled = true;
            console.log('Playing: ', viz.variables.animation.isPlaying());
        });
    });

fetch('国土数値情報の高速道路時系列データ（geojson形式）のURLを入力してください')
    .then(response => response.json())
    .then(function (data) {
        const source = new carto.source.GeoJSON(data);
        const railroadLayer = new carto.Layer('highway', source, new carto.Viz(`color: rgba(255, 255, 255, 1)`));
        railroadLayer.addTo(map);
    });
```

# 参考文献
https://carto.com/developers/carto-vl/guides/animated-visualizations/