title: Example
description: Synchronous geometry helpers for MapKit coordinate / region types: distance(a, b) (Haversine, meters), bearing(a, b) (degrees), regionContains(region, coordinate), regionFromCoordinates(coords, paddingFactor?). Pure functions, safe during render.
import {
Button, HStack, Map, Marker, Navigation, NavigationStack, Script,
ScrollView, Text, useEffect, useMemo, useObservable, useState, VStack,
} from "scripting"
import type { MapCoordinate } from "scripting"
const beijing: MapCoordinate = { latitude: 39.9042, longitude: 116.4074 }
const shanghai: MapCoordinate = { latitude: 31.2304, longitude: 121.4737 }
const guangzhou: MapCoordinate = { latitude: 23.1291, longitude: 113.2644 }
const sanFrancisco: MapCoordinate = { latitude: 37.7749, longitude: -122.4194 }
function DistanceBearingDemo() {
const [from, setFrom] = useState<MapCoordinate>(beijing)
const [to, setTo] = useState<MapCoordinate>(shanghai)
const d = useMemo(() => MapUtils.distance(from, to), [from, to])
const b = useMemo(() => MapUtils.bearing(from, to), [from, to])
return <VStack alignment={"leading"} spacing={8}>
<Text font={"headline"}>1. `distance` + `bearing`</Text>
<Text font={"caption"} foregroundStyle={"secondaryLabel"}>
Haversine distance (meters) and initial bearing (degrees, 0 = north,
90 = east) between two coordinates.
</Text>
<HStack spacing={8}>
<Button title="Beijing → Shanghai" buttonStyle="bordered"
action={() => { setFrom(beijing); setTo(shanghai) }} />
<Button title="Shanghai → Guangzhou" buttonStyle="bordered"
action={() => { setFrom(shanghai); setTo(guangzhou) }} />
<Button title="Beijing → SF" buttonStyle="bordered"
action={() => { setFrom(beijing); setTo(sanFrancisco) }} />
</HStack>
<Text monospaced>
{`from: ${from.latitude.toFixed(3)}, ${from.longitude.toFixed(3)}\n`}
{`to: ${to.latitude.toFixed(3)}, ${to.longitude.toFixed(3)}\n`}
{`distance: ${(d / 1000).toFixed(1)} km\n`}
{`bearing: ${b.toFixed(1)}°`}
</Text>
</VStack>
}
function RegionContainsDemo() {
// Region around People's Square, Shanghai
const region = {
center: { latitude: 31.2304, longitude: 121.4737 },
span: { latitudeDelta: 0.05, longitudeDelta: 0.05 },
}
const testPoints: { label: string; coord: MapCoordinate; inside: boolean }[] = useMemo(() => {
const probes = [
{ label: "Bund", coord: { latitude: 31.2407, longitude: 121.4905 } },
{ label: "Lujiazui", coord: { latitude: 31.2397, longitude: 121.4994 } },
{ label: "Pudong Intl Airport", coord: { latitude: 31.1443, longitude: 121.8083 } },
{ label: "Hangzhou", coord: { latitude: 30.2741, longitude: 120.1551 } },
]
return probes.map(p => ({ ...p, inside: MapUtils.regionContains(region, p.coord) }))
}, [])
return <VStack alignment={"leading"} spacing={8}>
<Text font={"headline"}>2. `regionContains`</Text>
<Text font={"caption"} foregroundStyle={"secondaryLabel"}>
Region: ~5 km box around People's Square. Probe a few points to see which
land inside.
</Text>
<VStack alignment={"leading"} spacing={4}>
{testPoints.map(p => (
<Text monospaced>
{p.inside ? "✓ inside " : "✗ outside "} {p.label}
</Text>
))}
</VStack>
</VStack>
}
function RegionFromCoordinatesDemo() {
const [paddingFactor, setPaddingFactor] = useState(0.1)
const coords = useMemo<MapCoordinate[]>(() => [
{ latitude: 31.2407, longitude: 121.4905 }, // Bund
{ latitude: 31.2304, longitude: 121.4737 }, // People's Square
{ latitude: 31.2397, longitude: 121.4994 }, // Lujiazui
{ latitude: 31.2229, longitude: 121.4583 }, // Nanjing Rd
], [])
const region = useMemo(
() => MapUtils.regionFromCoordinates(coords, paddingFactor),
[coords, paddingFactor]
)
// 用 cameraPosition (双向 observable) 而不是 initialCameraPosition,这样
// paddingFactor 变化时新计算的 region 会通过 setValue 推到地图。
// initialCameraPosition 顾名思义只在首次挂载生效,后续不响应 prop 变化。
const camera = useObservable<MapCameraPosition>(
region != null ? MapCameraPosition.region(region) : MapCameraPosition.automatic()
)
useEffect(() => {
if (region != null) {
camera.setValue(MapCameraPosition.region(region))
}
}, [region])
return <VStack alignment={"leading"} spacing={8}>
<Text font={"headline"}>3. `regionFromCoordinates`</Text>
<Text font={"caption"} foregroundStyle={"secondaryLabel"}>
Smallest enclosing region for 4 Shanghai landmarks. Padding expands the
bounding box outward.
</Text>
<HStack spacing={8}>
<Button title="tight (0)" buttonStyle="bordered" action={() => setPaddingFactor(0)} />
<Button title="10% pad" buttonStyle="bordered" action={() => setPaddingFactor(0.1)} />
<Button title="50% pad" buttonStyle="bordered" action={() => setPaddingFactor(0.5)} />
</HStack>
<Text font={"caption2"} foregroundStyle={"tertiaryLabel"}>
paddingFactor: {paddingFactor}{" · "}
span: {region?.span.latitudeDelta.toFixed(4)}° × {region?.span.longitudeDelta.toFixed(4)}°
</Text>
<Map
cameraPosition={camera}
frame={{ height: 280 }}
clipShape={{
type: 'rect',
cornerRadius: 12
}}
>
{coords.map(c => (
<Marker
coordinate={c}
tint="systemBlue"
/>
))}
</Map>
</VStack>
}
function Example() {
const dismiss = Navigation.useDismiss()
return <NavigationStack>
<ScrollView
navigationTitle="Map Utils"
toolbar={{
cancellationAction: <Button
title="Close"
action={dismiss}
/>
}}
>
<VStack
navigationTitle={"MapUtils"}
navigationBarTitleDisplayMode={"inline"}
spacing={24}
padding
>
<Text font={"caption"} foregroundStyle={"secondaryLabel"}>
Synchronous geometry helpers for MapKit coordinate / region types. Pure
functions, safe to call during render.
</Text>
<DistanceBearingDemo />
<RegionContainsDemo />
<RegionFromCoordinatesDemo />
</VStack>
</ScrollView>
</NavigationStack>
}
async function run() {
await Navigation.present({ element: <Example /> })
Script.exit()
}
run()
