この記事は FOSS4G Advent Calendar 2025 の 25 日目の記事です。
はじめに
クリスマスイブの夜、サンタクロースは世界中の子どもたちにプレゼントを届けるために空を駆け巡っています。そんなサンタクロースが今どこにいるのか、気になったことはありませんか?
今回は、サンタクロースがどのようなルートを辿っているのかを地図上で可視化するアプリケーションを作ってみました。
サンタクロースを追跡するサービスたち
サンタクロースを追跡するサービスは Google Santa Tracker や Flightradar24 などが存在します。
このようなサービスのように「自分でもサンタクロースの動きを可視化してみたい!」という気持ちがありまして、今回はサンタクロースのルートデータを取得して、MapLibreで可視化してみることにしました。
サンタクロースのルートデータについて
サンタクロースのルート情報は、とある場所から取得することができます(具体的な取得先についてはここでは触れません)。
ルートを GeoJSON に変換
サンタクロースのルートデータを、以下のような GeoJSON に変換します。
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [経度, 緯度]
},
"properties": {
"id": "都市ID",
"city": "都市名",
"region": "地域名",
"arrival": 1577181600000,
"departure": 1577181900000,
"population": 12345,
"presentsDelivered": 67890,
"details": {
"timezone": 32400
}
}
}
]
}
| プロパティ | 説明 |
|---|---|
arrival |
その都市への到着時刻(ミリ秒、Unixタイムスタンプ) |
departure |
その都市からの出発時刻(ミリ秒、Unixタイムスタンプ) |
details.timezone |
現地のタイムゾーン情報(秒単位のUTCオフセット) |
このデータの特徴として 経由する都市のポイント情報は提供されていますが、都市間を移動するルートの線形情報は含まれていません 。つまり、サンタクロースの軌跡を線で表示したい場合は、自分で計算する必要があります。
今回作るアプリケーションの概要
今回のアプリケーションは、以下の技術スタックで実装しました。なお、今回はバイブコーディングで実装を進めています。
- MapLibre GL JS: 地図表示エンジン
- Vue.js 3: フロントエンドフレームワーク
- Turf.js: 地理空間計算ライブラリ
このアプリケーションでは、以下のような機能を実装しています。
- タイムスライダーで選択した時刻におけるサンタクロースの現在地を表示
- サンタクロースが経由した地点と、その地点に到着した時刻を現地時刻で表示
- サンタクロースが通った軌跡を線で表示
MapLibreのGlobe表示
MapLibre GL JSはGlobe(地球儀)表示に対応しています。スタイルの読み込みが完了した後に設定します。
map = new maplibregl.Map({
container: mapContainer.value,
style: 'https://tile.openstreetmap.jp/styles/osm-bright-ja/style.json',
center: [0, 30],
zoom: 3,
})
map.on('style.load', () => {
map.setProjection({ type: 'globe' })
})
サンタクロースの位置計算
指定した時刻におけるサンタクロースの現在位置と軌跡は、calculateTrailPath関数で計算しています。この関数は軌跡の座標配列を返し、その最後の座標がサンタクロースの現在位置となります。
// 軌跡を計算(最後の座標がサンタの現在位置)
const trail = calculateTrailPath(currentTime.value)
if (trail.length > 0) {
// 軌跡の最後の座標 = サンタの現在位置
const position = trail[trail.length - 1] as [number, number]
santaMarker.setLngLat(position)
}
// 軌跡をMapLibreのソースに反映
updateTrail(trail)
calculateTrailPath関数の全体は以下のとおりです。この関数では、指定された時刻(timestamp)に基づいて、サンタクロースが通った軌跡の座標配列を生成しています。
const calculateTrailPath = (timestamp: number): number[][] => {
if (!santaData.value) return []
const features = santaData.value.features
const coordinates: number[][] = []
// 最初の地点を追加
coordinates.push([...features[0].geometry.coordinates])
for (let i = 0; i < features.length - 1; i++) {
const currentStop = features[i]
const nextStop = features[i + 1]
const departure = currentStop.properties.departure
const nextArrival = nextStop.properties.arrival
// 現在時刻がこの地点の出発時刻より前なら終了
if (timestamp < departure) {
break
}
// 次の地点への移動を計算
if (timestamp >= departure) {
const from = turf.point(currentStop.geometry.coordinates)
const to = turf.point(nextStop.geometry.coordinates)
// 大圏航路を計算
const greatCircle = turf.greatCircle(from, to, { npoints: 100 })
// 移動完了している場合
if (timestamp >= nextArrival) {
// greatCircleの結果を座標配列に追加
if (greatCircle.geometry.type === 'LineString') {
// LineStringの場合はそのまま追加(最初の点は既に追加済みなのでスキップ)
const gcCoords = fixAntimeridian(greatCircle.geometry.coordinates)
for (let j = 1; j < gcCoords.length; j++) {
coordinates.push(gcCoords[j])
}
} else if (greatCircle.geometry.type === 'MultiLineString') {
// MultiLineStringの場合は結合してfixAntimeridianを適用
const allCoords: number[][] = []
for (const lineCoords of greatCircle.geometry.coordinates) {
allCoords.push(...lineCoords)
}
const gcCoords = fixAntimeridian(allCoords)
for (let j = 1; j < gcCoords.length; j++) {
coordinates.push(gcCoords[j])
}
}
} else {
// 移動中の場合、補間した位置まで
const travelDuration = nextArrival - departure
const elapsed = timestamp - departure
const ratio = elapsed / travelDuration
// 大圏航路上の距離を計算
const totalDistance = turf.distance(from, to, { units: 'kilometers' })
const traveledDistance = totalDistance * ratio
// greatCircleの結果をLineStringに変換(日付変更線対応)
let lineCoords: number[][] = []
if (greatCircle.geometry.type === 'LineString') {
lineCoords = greatCircle.geometry.coordinates
} else if (greatCircle.geometry.type === 'MultiLineString') {
// MultiLineStringの場合は結合
for (const lineSegment of greatCircle.geometry.coordinates) {
lineCoords.push(...lineSegment)
}
}
// 日付変更線をまたぐ場合の座標を修正してLineStringに変換
const fixedCoords = fixAntimeridian(lineCoords)
const lineString = turf.lineString(fixedCoords)
// lineSliceAlongで進行距離に応じた部分を取得
const sliced = turf.lineSliceAlong(lineString, 0, traveledDistance, {
units: 'kilometers',
})
// スライスした座標を追加(最初の点は既に追加済みなのでスキップ)
for (let j = 1; j < sliced.geometry.coordinates.length; j++) {
coordinates.push([...sliced.geometry.coordinates[j]])
}
break
}
}
}
// 日付変更線をまたぐ座標を修正
return fixAntimeridian(coordinates)
}
都市間の軌跡は、Turf.jsのgreatCircle関数を使って大圏航路を計算します。この関数は東経と西経を跨ぐ場合にMultiLineStringを返すことがあるため、MultiLineStringの場合は、複数の線分を1つの配列に結合してからfixAntimeridianで座標を補正します。
サンタクロースが2つの都市の間を移動中の場合は、経過時間の割合(ratio)から進行距離を計算し、lineSliceAlong関数で大圏航路上の途中位置までの軌跡を切り出します。lineSliceAlong関数は入力がLineStringである必要があるため、前述のようにMultiLineStringを事前に結合しておくことが重要です。
東経・西経を跨ぐ問題への対処
サンタクロースは世界中を駆け巡るため、東経と西経を跨いで移動することがあります。ここで問題になるのが、日付変更線(経度±180度)を超える場合の線形描画です。何も対策をしないと、このように線が地球を1周してしまいます。
これを修正するために、座標を正規化する関数を実装する必要があります。連続する2点の経度差が±180度を超える場合、東経と西経を跨いでいると判定して座標を補正しています。
const fixAntimeridian = (coordinates: number[][]): number[][] => {
const newCoords = [...coordinates.map((coord) => [...coord])]
for (let i = 1; i < newCoords.length; i++) {
const prevLon = newCoords[i - 1][0]
let currLon = newCoords[i][0]
const diff = currLon - prevLon
// 東へ進んで日付変更線を越えた場合
// 例: 170° → -170° の場合、差は -340°
// -170° + 360° = 190° に補正
if (diff < -180) {
currLon += 360
}
// 西へ進んで日付変更線を越えた場合
// 例: -170° → 170° の場合、差は 340°
// 170° - 360° = -190° に補正
else if (diff > 180) {
currLon -= 360
}
newCoords[i][0] = currLon
}
return newCoords
}
タイムスライダーの実装
タイムスライダーでは、再生速度を調整しながらサンタクロースの移動をアニメーションで追跡できるようにしています。
const animate = () => {
if (!isPlaying.value) return
const now = performance.now()
const delta = now - lastTimestamp
lastTimestamp = now
// playbackSpeed: 1 = 1秒で実際の60秒進む
const increment = (delta / 1000) * 60 * 1000 * playbackSpeed.value
const newTime = currentTime.value + increment
if (newTime >= props.maxTime) {
updateTime(props.maxTime)
pause()
} else {
updateTime(newTime)
animationFrameId = requestAnimationFrame(animate)
}
}
実際に動かしてみる
これらの実装を組み合わせて、サンタクロースの追跡アプリケーションが完成しました!
タイムスライダーをスライドさせると、指定した時刻におけるサンタクロースの位置と、それまでに通過した軌跡が表示されます。訪問済みの都市には到着時刻が現地時間で表示されるので、サンタクロースがいつその街を訪れたのかがわかります。
こんなことができるかも?
サンタクロースの位置情報がリアルタイムでわかれば、「サンタクロースがあと100km先に来ています!」という通知を出して、何かアクションを起こすトリガーにできるかもしれません。
ぜひ、みなさんもサンタクロースの移動軌跡を使ったアプリケーションを作ってみてはいかがでしょうか。


