LoginSignup
3
4

More than 1 year has passed since last update.

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

Posted at

はじめに

  • 警察庁の交通事故統計情報のオープンデータCARTO VLでアニメーション化してみました。
  • CARTO VLは、時系列データのアニメーション化等ができる、オープンソースのJavaScriptライブラリです。
  • 今回用いているCARTO VLのバージョンはv1.4.4です。
  • 警察庁オープンデータは、2019年の本票を用いています。

アウトプットイメージ

前提条件

  • アニメーション化で用いる、警察庁オープンデータ(2019年・本票)は、CSV形式のデータになっていますので、これをQGIS等でgeojson形式のデータに変換しています。
  • 変換後のgeojson形式のデータは、以下のとおりです。
    image.png
  • 警察庁オープンデータ(2019年・本票)は、以下のとおり加工しています。
  • 「発⽣⽇時 年」、「発⽣⽇時 ⽉」、「発⽣⽇時 ⽇」、「発⽣⽇時 時」、「発⽣⽇時 分」をもとに、「date_time」を作成。
  • 「事故内容」をもとに、「jiko_details」を作成(1=死亡、2=負傷)。
  • 「地点 緯度(北緯)」、「地点 経度(東経)」をもとに、「coordinates」を作成。
  • また、国土数値情報の高速道路時系列データ(geojson形式)を用いています。
  • 背景地図には、Mapboxの地図を用いているため、別途、アクセストークンの取得が必要です。

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

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

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);
    });

参考文献

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4