はじめに
- 警察庁の交通事故統計情報のオープンデータをCARTO VLでアニメーション化してみました。
- CARTO VLは、時系列データのアニメーション化等ができる、オープンソースのJavaScriptライブラリです。
- 今回用いているCARTO VLのバージョンはv1.4.4です。
- 警察庁オープンデータは、2019年の本票を用いています。
アウトプットイメージ
警察庁オープンデータ(2019年、38万件)をCARTO VL(https://t.co/55wQvvJnzZ)というライブラリーで時系列で表示してみた。青:負傷事故、赤:死亡事故。時系列データのアニメーションがさくっとできて良い📝#CARTOVL #Mapbox
— shi_works🌥️ (@syanseto) April 22, 2022
【警察庁】交通事故統計情報のオープンデータhttps://t.co/YsIpol9K2Z https://t.co/toMaO8fELe pic.twitter.com/MJh585koJY
前提条件
- アニメーション化で用いる、警察庁オープンデータ(2019年・本票)は、CSV形式のデータになっていますので、これをQGIS等でgeojson形式のデータに変換しています。
- 変換後のgeojson形式のデータは、以下のとおりです。
- 警察庁オープンデータ(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);
});
参考文献