Skip to content

Instantly share code, notes, and snippets.

@deton
Forked from kkdd/README.md
Last active April 14, 2024 03:09
Show Gist options
  • Save deton/57ae19aed61b03e954df43fa10aa5584 to your computer and use it in GitHub Desktop.
Save deton/57ae19aed61b03e954df43fa10aa5584 to your computer and use it in GitHub Desktop.
deck.gl trips-layer animation with moving icon: reading trips files dropped-onto

getting trips-json files

$ cat trips.json 
[{"vendor":"car","path":[[139.8007914,35.5144044],[139.9108676,35.4396142]],"timestamps":[0,600]},{"vendor":"escooter","path":[[139.9108139,35.439529],[139.8008987,35.5140027]],"timestamps":[0,600]}]
$ curl https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/trips/trips-v7.json 2> /dev/null | jq -c 'map(select(.path[0][0] < -73.98 and .path[-1][0] < -73.98 and .path[0][0] > -74.02 and .path[-1][0] > -74.02 and .path[0][1] < 40.75 and .path[-1][1] < 40.75 and .path[0][1] > 40.70 and .path[-1][1] > 40.70))' | jq -c '.' > trips-v7_0.json
<html>
<head>
<!-- deck.gl standalone bundle -->
<script src="https://unpkg.com/deck.gl@^8.8.0/dist.min.js"></script>
<script src="https://unpkg.com/@deck.gl/carto@^8.8.0/dist.min.js"></script>
<!-- Maplibre dependencies -->
<script src='https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.js'></script>
<link href='https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.css' rel='stylesheet' />
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@6/turf.min.js"></script>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,1,0" />
<style>
body { margin:0; padding:0; }
#map { width: 100vw; height: 100vh; }
.map-overlay {
position: absolute;
width: 25%;
top: 0;
left: 0;
padding: 10px;
}
</style>
</head>
<body>
<div id='map'></div>
<div class="map-overlay top">
<div class="map-overlay-inner">
<input id="slider" type="range" min="0" value="0" />
</div>
<div class="map-overlay-inner">
<button id="pauseButton">
<span class="material-symbols-outlined">pause</span>
</button>
<button id="playButton">
<span class="material-symbols-outlined">play_arrow</span>
</button>
</div>
</div>
</body>
<script type="text/javascript">
const ANIMATION_SPEED = 40;
const INITIAL_PAUSE = 1;
const ICONS = {
car: {
url: "https://deton.github.io/folium_tripslayer/car.svg",
width: 24,
height: 24,
mask: true
},
escooter: {
url: "https://deton.github.io/folium_tripslayer/escooter.svg",
width: 24,
height: 24,
mask: true
},
walk: {
url: "https://deton.github.io/folium_tripslayer/walk.svg",
width: 24,
height: 24,
mask: true
},
};
const colors = {
0: [253, 128, 93],
'car': [128, 0, 128],
'escooter': [102, 205, 170],
'walk': [250, 128, 114],
'others': [23, 184, 190],
};
const getColor = i => {
if (typeof i === 'undefined' || !(i in colors)) {i = 'others'};
return colors[i];
}
const getIcon = i => {
if (typeof i === 'undefined' || !(i in ICONS)) {i = 'car'}
return ICONS[i];
}
const slider = document.getElementById("slider");
const pauseButton = document.getElementById("pauseButton");
const playButton = document.getElementById("playButton");
pauseButton.onclick = onClickPause;
let reqAnimFrameId;
let timerId;
function onClickPause() {
window.cancelAnimationFrame(reqAnimFrameId);
clearInterval(timerId);
}
//drawTrips([{"vendor":"car","path":[[139.8007914,35.5144044],[139.9108676,35.4396142]],"timestamps":[0,600]},{"vendor":"escooter","path":[[139.9108139,35.439529],[139.8008987,35.5140027]],"timestamps":[0,600]}]);
drawTrips([{
vendor: 'car',
path: [
[ -4.4214296, 36.73835 ],
[ -4.422104, 36.737865 ],
[ -4.4229302, 36.73773 ],
[ -4.4235334, 36.735817 ],
[ -4.4222927, 36.73413 ],
[ -4.4218254, 36.732475 ],
[ -4.4213734, 36.72983 ],
[ -4.420156, 36.73 ],
[ -4.419239, 36.730686 ],
[ -4.417272, 36.732136 ],
],
timestamps: [0, 60, 120, 180, 240, 300, 360, 420, 480, 540]
}, {
vendor: 'walk',
path: [
[ -4.4155564, 36.732613 ],
[ -4.4147606, 36.729523 ],
[ -4.4143534, 36.728085 ],
[ -4.414023, 36.727142 ],
[ -4.4145956, 36.726017 ],
[ -4.4163203, 36.722366 ],
],
timestamps: [60, 120, 180, 240, 300, 360]
}, {
vendor: 'escooter',
path: [
[ -4.4163203, 36.722366 ],
[ -4.4142747, 36.72012 ],
[ -4.4162464, 36.71957 ],
[ -4.418931, 36.71882 ],
[ -4.421059, 36.718254 ],
[ -4.421595, 36.718174 ],
],
timestamps: [360, 420, 480, 540, 600, 660]
}, {
vendor: 'walk',
path: [
[ -4.421595, 36.718174 ],
[ -4.424712, 36.717197 ],
[ -4.4268923, 36.717003 ],
[ -4.427205, 36.717583 ],
[ -4.426953, 36.717876 ],
[ -4.4264026, 36.715973 ],
],
timestamps: [660, 720, 780, 840, 900, 960]
}, {
vendor: 'walk',
path: [
[ -4.4454684, 36.702717 ],
[ -4.440271, 36.704346 ],
[ -4.4393854, 36.70489 ],
[ -4.437068, 36.706684 ],
[ -4.4344425, 36.70879 ],
[ -4.4314194, 36.71117 ],
[ -4.4300385, 36.71217 ],
[ -4.4270782, 36.714962 ],
[ -4.4267263, 36.71531 ],
],
timestamps: [0, 60, 120, 180, 240, 300, 360, 420, 480]
}]);
const setDropArea = (area) => {
area.ondragover = () => false;
area.ondrop = async event => {
event.preventDefault();
const promises = [];
for (const file of event.dataTransfer.files) {
if (file.name.split('.').pop() == 'json') {
promises.push(file.text());
}
}
const results = await Promise.all(promises);
drawTrips(results.map(JSON.parse).flat());
return false;
};
}
setDropArea(document.getElementById('map'));
function bbox(json) {
let points = [];
for (let e of json){
points = points.concat(e.path);
}
const b = turf.bbox(turf.multiPoint(points))
return [[b[0], b[3]], [b[2], b[1]]];
}
function interval_timestamps(json) {
let timestamps = [];
for (let e of json){
timestamps = timestamps.concat(e.timestamps);
}
return [Math.min(...timestamps), Math.max(...timestamps)];
}
function drawTrips(json) {
onClickPause();
const interval = interval_timestamps(json);
interval[0] -= ANIMATION_SPEED * INITIAL_PAUSE;
const view = new deck.WebMercatorViewport({width: document.body.clientWidth, height: document.body.clientHeight});
const deckgl = new deck.DeckGL({
container: 'map',
mapStyle: deck.carto.BASEMAP.POSITRON,
initialViewState: view.fitBounds(bbox(json)),
controller: true
});
slider.max = String(interval[1] - interval[0]);
slider.oninput = onSliderValueChange;
let time = 0;
function onSliderValueChange(ev) {
time = Number(ev.target.value);
setDeckGLProps();
}
const RATE_ANIMATION_FRAME = 50;
function animate() {
time = (time + ANIMATION_SPEED/RATE_ANIMATION_FRAME) % (interval[1] - interval[0]);
slider.value = String(time);
reqAnimFrameId = window.requestAnimationFrame(animate);
}
function setDeckGLProps() {
deckgl.setProps({
layers: [
new deck.TripsLayer({
id: 'trips-layer',
data: json,
getPath: d => d.path,
getTimestamps: d => d.timestamps,
getColor: d => getColor(d.vendor),
opacity: 0.8,
widthMinPixels: 3,
jointRounded: true,
capRounded: true,
trailLength: 180,
currentTime: time + interval[0],
shadowEnabled: false
}),
// https://stackoverflow.com/questions/59636420/is-it-possible-to-animate-moving-icons-in-deck-gl
new deck.IconLayer({
id: 'icon-layer',
data: json,
sizeScale: 5,
getColor: d => getColor(d.vendor),
getIcon: d => getIcon(d.vendor),
getSize: d => 5,
getPixelOffset: [0, -10],
getPosition: (d) => {
let x = null;
let y = null;
const t = time + interval[0];
for (let i = 0; i < d.timestamps.length-1; i++) {
if (t < d.timestamps[i + 1] && t >= d.timestamps[i]) {
let frac = (t - d.timestamps[i]) / (d.timestamps[i+1] - d.timestamps[i]);
x = ((d.path[i+1][0] - d.path[i][0]) * frac) + d.path[i][0];
y = ((d.path[i+1][1] - d.path[i][1]) * frac) + d.path[i][1];
break;
}
}
return [x, y];
},
updateTriggers: {
getPosition: time
}
})
]
});
}
playButton.onclick = onClickPlay;
function onClickPlay() {
timerId = setInterval(setDeckGLProps, 50);
reqAnimFrameId = window.requestAnimationFrame(animate);
}
onClickPlay();
}
</script>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment