コンテンツにスキップ

Spherical Utilities(球面ユーティリティ)

球面ユーティリティは、球面地球モデルを使用した正確な地理計算を提供します。これらの関数は、距離測定、パス計算、地理的計算に不可欠です。

Spherical オブジェクトには、地球の表面上の距離、方位角、位置を計算するためのユーティリティ関数が含まれています。WGS84 楕円体に基づく地球の半径を使用した球面地球モデルを使用します。

import com.mapconductor.core.spherical.Spherical
  • 地球の半径: 6,378,137 メートル(WGS84 楕円体の長半径)
  • 座標系: WGS84 地理座標(緯度/経度)
fun computeDistanceBetween(from: GeoPointInterface, to: GeoPointInterface): Double

ハバーサイン公式を使用して 2 点間の最短距離を計算します:

val sanFrancisco = GeoPoint.fromLatLong(37.7749, -122.4194)
val newYork = GeoPoint.fromLatLong(40.7128, -74.006)
val distanceMeters = Spherical.computeDistanceBetween(sanFrancisco, newYork)
val distanceKm = distanceMeters / 1000.0
val distanceMiles = distanceMeters / 1609.344
println("Distance: ${distanceKm.toInt()} km (${distanceMiles.toInt()} miles)") // ハバーサイン公式を使用して 2 点間の最短距離を計算
fun computeLength(path: List<GeoPointInterface>): Double

複数の点で構成されるパスの総長を計算します:

val routePoints = listOf(
GeoPoint.fromLatLong(37.7749, -122.4194), // San Francisco
GeoPoint.fromLatLong(37.7849, -122.4094), // North Beach
GeoPoint.fromLatLong(37.7949, -122.3994), // Russian Hill
GeoPoint.fromLatLong(37.8049, -122.3894) // Fisherman's Wharf
)
val totalDistance = Spherical.computeLength(routePoints)
println("Route length: ${(totalDistance / 1000).toInt()} km") // 複数の点で構成されるパスの総長を計算
fun computeHeading(from: GeoPointInterface, to: GeoPointInterface): Double

ある点から別の点への初期方位角(方向)を計算します:

val start = GeoPoint.fromLatLong(37.7749, -122.4194)
val destination = GeoPoint.fromLatLong(40.7128, -74.0060)
val bearing = Spherical.computeHeading(start, destination)
println("Head ${bearing.toInt()}° from San Francisco to reach New York") // ある点から別の点への初期方位角(方向)を計算
// Convert to compass direction
val compassDirection = when {
bearing >= -22.5 && bearing < 22.5 -> "North"
bearing >= 22.5 && bearing < 67.5 -> "Northeast"
bearing >= 67.5 && bearing < 112.5 -> "East"
bearing >= 112.5 && bearing < 157.5 -> "Southeast"
bearing >= 157.5 || bearing < -157.5 -> "South"
bearing >= -157.5 && bearing < -112.5 -> "Southwest"
bearing >= -112.5 && bearing < -67.5 -> "West"
else -> "Northwest"
}
fun computeOffset(origin: GeoPointInterface, distance: Double, heading: Double): GeoPoint

原点からの距離と方向を指定して新しい位置を計算します:

val origin = GeoPoint.fromLatLong(37.7749, -122.4194)
// Points around the origin
val north1km = Spherical.computeOffset(origin, 1000.0, 0.0) // 1km north
val east1km = Spherical.computeOffset(origin, 1000.0, 90.0) // 1km east
val south1km = Spherical.computeOffset(origin, 1000.0, 180.0) // 1km south
val west1km = Spherical.computeOffset(origin, 1000.0, 270.0) // 1km west
// Create a square around the origin
val squarePoints = listOf(north1km, east1km, south1km, west1km, north1km)
fun computeOffsetOrigin(to: GeoPointInterface, distance: Double, heading: Double): GeoPoint?

目的地、距離、元の方位角を指定して原点を計算します:

val destination = GeoPoint.fromLatLong(37.7849, -122.4094)
val distance = 1000.0 // 1km
val originalHeading = 45.0 // Northeast
val origin = Spherical.computeOffsetOrigin(destination, distance, originalHeading)
origin?.let { point ->
println("Started from: ${point.latitude}, ${point.longitude}")
}
fun interpolate(from: GeoPointInterface, to: GeoPointInterface, fraction: Double): GeoPoint

2 点間の大円パスに沿って補間します:

val start = GeoPoint.fromLatLong(37.7749, -122.4194)
val end = GeoPoint.fromLatLong(40.7128, -74.0060)
// 大円ルート沿いのウェイポイントを作成
val waypoints = (0..10).map { i ->
val fraction = i / 10.0
Spherical.interpolate(start, end, fraction)
}
// Use in a route animation
@Composable
fun AnimatedRoute() {
var currentWaypoint by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (currentWaypoint < waypoints.size - 1) {
delay(1000)
currentWaypoint++
}
}
// MapView を GoogleMapView、MapboxMapView などの選択した地図SDKに置き換えてください
MapView(state = mapViewState) {
// Show route
Polyline(
points = waypoints,
strokeColor = Color.Blue,
strokeWidth = 3.dp
)
// Moving marker
if (currentWaypoint < waypoints.size) {
Marker(
position = waypoints[currentWaypoint],
icon = DefaultIcon(fillColor = Color.Red)
)
}
}
}
fun linearInterpolate(from: GeoPointInterface, to: GeoPointInterface, fraction: Double): GeoPoint

地球の曲率を考慮しない高速線形補間:

val start = GeoPoint.fromLatLong(37.7749, -122.4194)
val end = GeoPoint.fromLatLong(37.7849, -122.4094) // Short distance
// For small distances, linear interpolation is faster and sufficiently accurate
val midpoint = Spherical.linearInterpolate(start, end, 0.5)
fun computeArea(path: List<GeoPointInterface>): Double
fun computeSignedArea(path: List<GeoPointInterface>): Double

閉じたポリゴンの面積を計算します:

val polygonPoints = listOf(
GeoPoint.fromLatLong(37.7749, -122.4194),
GeoPoint.fromLatLong(37.7849, -122.4094),
GeoPoint.fromLatLong(37.7849, -122.4294),
GeoPoint.fromLatLong(37.7749, -122.4294),
GeoPoint.fromLatLong(37.7749, -122.4194) // Close the polygon
)
val area = Spherical.computeArea(polygonPoints)
val areaKm2 = area / 1_000_000 // Convert to square kilometers
println("Polygon area: ${areaKm2.toInt()} km²")
// Signed area tells you orientation
val signedArea = Spherical.computeSignedArea(polygonPoints)
val orientation = if (signedArea > 0) "Counter-clockwise" else "Clockwise"
println("Polygon orientation: $orientation")
@Composable
fun ProximityAlert() {
val targetLocation = GeoPoint.fromLatLong(37.7749, -122.4194)
val alertRadius = 500 // 500 meters
var userLocation by remember { mutableStateOf<GeoPointInterface?>(null) }
var isNearTarget by remember { mutableStateOf(false) }
// ユーザー位置が変わったときに近接状態を更新
LaunchedEffect(userLocation) {
userLocation?.let { location ->
val distance = Spherical.computeDistanceBetween(location, targetLocation)
isNearTarget = distance <= alertRadius
}
}
// MapView を GoogleMapView、MapboxMapView などの選択した地図SDKに置き換えてください
MapView(state = mapViewState) {
// Target location
Marker(
position = targetLocation,
icon = DefaultIcon(
fillColor = if (isNearTarget) Color.Green else Color.Red,
label = "Target"
)
)
// Alert radius
Circle(
center = targetLocation,
radiusMeters = alertRadius,
strokeColor = Color.Blue,
fillColor = Color.Blue.copy(alpha = 0.2f)
)
// User location if available
userLocation?.let { location ->
Marker(
position = location,
icon = DefaultIcon(fillColor = Color.Blue, label = "You")
)
}
}
if (isNearTarget) {
Text(
text = "ターゲットの近くにいます!",
color = Color.Green,
fontWeight = FontWeight.Bold
)
}
}
@Composable
fun RouteProgress() {
val route = listOf(
GeoPoint.fromLatLong(37.7749, -122.4194),
GeoPoint.fromLatLong(37.7849, -122.4094),
GeoPoint.fromLatLong(37.7949, -122.3994)
)
var currentPosition by remember { mutableStateOf(route.first()) }
val totalDistance = Spherical.computeLength(route)
// Calculate progress along route
fun calculateProgress(position: GeoPointInterface): Double {
var distanceToPosition = 0.0
var minDistanceToRoute = Double.MAX_VALUE
var bestSegmentProgress = 0.0
// Find closest point on route
for (i in 0 until route.size - 1) {
val segmentStart = route[i]
val segmentEnd = route[i + 1]
val segmentLength = Spherical.computeDistanceBetween(segmentStart, segmentEnd)
// Find closest point on this segment
var closestFraction = 0.0
var minSegmentDistance = Double.MAX_VALUE
for (fraction in 0..100) {
val testFraction = fraction / 100.0
val testPoint = Spherical.interpolate(segmentStart, segmentEnd, testFraction)
val distance = Spherical.computeDistanceBetween(position, testPoint)
if (distance < minSegmentDistance) {
minSegmentDistance = distance
closestFraction = testFraction
}
}
if (minSegmentDistance < minDistanceToRoute) {
minDistanceToRoute = minSegmentDistance
bestSegmentProgress = distanceToPosition + (segmentLength * closestFraction)
}
distanceToPosition += segmentLength
}
return bestSegmentProgress / totalDistance
}
val progress = calculateProgress(currentPosition)
Column {
Text("Route Progress: ${(progress * 100).toInt()}%")
LinearProgressIndicator(progress = progress.toFloat())
// MapView を GoogleMapView、MapboxMapView などの選択した地図SDKに置き換えてください
MapView(state = mapViewState) {
// Show route
Polyline(
points = route,
strokeColor = Color.Blue,
strokeWidth = 4.dp
)
// Current position
Marker(
position = currentPosition,
icon = DefaultIcon(fillColor = Color.Red, label = "Current")
)
}
}
}
@Composable
fun GeofenceExample() {
val geofenceCenter = GeoPoint.fromLatLong(37.7749, -122.4194)
val geofenceRadius = 1000 // 1km
var userLocation by remember { mutableStateOf<GeoPointInterface?>(null) }
var insideGeofence by remember { mutableStateOf(false) }
// Check geofence status
LaunchedEffect(userLocation) {
userLocation?.let { location ->
val distance = Spherical.computeDistanceBetween(location, geofenceCenter)
insideGeofence = distance <= geofenceRadius
}
}
// MapView を GoogleMapView、MapboxMapView などの選択した地図SDKに置き換えてください
MapView(state = mapViewState) {
// Geofence boundary
Circle(
center = geofenceCenter,
radiusMeters = geofenceRadius,
strokeColor = if (insideGeofence) Color.Green else Color.Red,
strokeWidth = 3.dp,
fillColor = Color.Blue.copy(alpha = 0.1f)
)
userLocation?.let { location ->
Marker(
position = location,
icon = DefaultIcon(
fillColor = if (insideGeofence) Color.Green else Color.Red,
label = if (insideGeofence) "内部" else "外部"
)
)
}
}
}
  1. 適切なメソッドを選択: 短距離と頻繁な計算には linearInterpolate を使用
  2. 結果をキャッシュ: 可能な場合は計算された距離と方位角を保存
  3. バッチ計算: 複数の点をまとめて処理し、関数呼び出しのオーバーヘッドを削減
  4. 精度 vs パフォーマンス: ユースケースに完全な球面精度が必要かどうかを検討
  1. 座標検証: 計算前に入力座標が有効であることを確認
  2. エラー処理: computeOffsetOrigin からの null 結果を確認
  3. 単位の一貫性: すべての距離はメートル、すべての角度は度数
  4. 地球モデル: これは球面地球モデルを使用し、より正確な楕円体モデルではありません
  5. 精度: ほとんどのマッピングアプリケーションには十分な精度の結果ですが、高精度測量には小さな誤差がある可能性があります