示例 PRO

import {
  Button, Color, HStack, Image, MapCoordinate, MapRegion, Navigation, NavigationStack,
  Picker, Script, ScrollView, Text, useState, VStack, ZStack,
} from "scripting"

type POI = { name: string; coord: MapCoordinate; tint: Color }

type PresetDef = {
  region: MapRegion
  pois: POI[]
}

// Each preset ships with a couple of named POIs nearby — typical
// "widget preview" pattern: render a static map with multiple labeled pins.
const PRESETS: Record<string, PresetDef> = {
  bund: {
    region: {
      center: { latitude: 31.2407, longitude: 121.4905 },
      span: { latitudeDelta: 0.025, longitudeDelta: 0.025 },
    },
    pois: [
      { name: "Bund", coord: { latitude: 31.2407, longitude: 121.4905 }, tint: "systemRed" },
      { name: "Lujiazui", coord: { latitude: 31.2397, longitude: 121.4994 }, tint: "systemBlue" },
      { name: "Yu Garden", coord: { latitude: 31.2275, longitude: 121.4920 }, tint: "systemPurple" },
    ],
  },
  apple_park: {
    region: {
      center: { latitude: 37.3349, longitude: -122.0090 },
      span: { latitudeDelta: 0.015, longitudeDelta: 0.015 },
    },
    pois: [
      { name: "Apple Park", coord: { latitude: 37.3349, longitude: -122.0090 }, tint: "systemRed" },
      { name: "Visitor Center", coord: { latitude: 37.3327, longitude: -122.0053 }, tint: "systemTeal" },
    ],
  },
  golden_gate: {
    region: {
      center: { latitude: 37.8199, longitude: -122.4783 },
      span: { latitudeDelta: 0.025, longitudeDelta: 0.025 },
    },
    pois: [
      { name: "Bridge North", coord: { latitude: 37.8324, longitude: -122.4795 }, tint: "systemOrange" },
      { name: "Bridge South", coord: { latitude: 37.8070, longitude: -122.4750 }, tint: "systemOrange" },
      { name: "Fort Point", coord: { latitude: 37.8104, longitude: -122.4769 }, tint: "systemBrown" },
    ],
  },
}

type Preset = keyof typeof PRESETS

const SNAP_W = 320
const SNAP_H = 220

function Example() {
  const [preset, setPreset] = useState<Preset>("bund")
  const [style, setStyle] = useState<"standard" | "imagery" | "hybrid">("standard")
  const [appearance, setAppearance] = useState<"light" | "dark">("light")
  const [snap, setSnap] = useState<MapSnapshot | null>(null)
  const [loading, setLoading] = useState(false)
  const [err, setErr] = useState<string | null>(null)

  const def = PRESETS[preset]

  const take = async () => {
    setLoading(true); setErr(null)
    try {
      const result = await MapSnapshotter.take({
        region: def.region,
        size: { width: SNAP_W, height: SNAP_H },
        mapStyle: { style },
        appearance,
      })
      setSnap(result)
    } catch (e) {
      setErr(String(e))
    } finally {
      setLoading(false)
    }
  }

  // For each POI of the active preset, project the geo coordinate onto the
  // snapshot's point space. Clip to snapshot bounds so off-screen pins drop
  // out instead of bleeding past the image edge.
  const overlays = snap != null
    ? def.pois.map(p => {
        const pt = snap.point(p.coord)
        const inBounds =
          pt.x >= 0 && pt.y >= 0 && pt.x <= snap.size.width && pt.y <= snap.size.height
        return { ...p, pt, inBounds }
      })
    : []

  const dismiss = Navigation.useDismiss()

  return <NavigationStack>
    <ScrollView
      navigationTitle="Map Snapshotter"
      toolbar={{
        cancellationAction: <Button
          title="Close"
          action={dismiss}
        />
      }}
    >
      <VStack
        navigationTitle={"MapSnapshotter"}
        navigationBarTitleDisplayMode={"inline"}
        spacing={16}
        padding
      >
        <Text font={"caption"} foregroundStyle={"secondaryLabel"}>
          {`MapSnapshotter.take(...) renders a static map offscreen and returns
          a MapSnapshot { image, point }. The image
          below is the snapshot's image fed into <Image image={snap.image}>.`}
        </Text>

        <VStack alignment={"leading"} spacing={8}>
          <Text font={"headline"}>Preset region</Text>
          <Picker
            title="Region"
            value={preset}
            onChanged={(v: any) => setPreset(v as Preset)}
            pickerStyle={"segmented"}
          >
            <Text tag={"bund"}>Bund</Text>
            <Text tag={"apple_park"}>Apple Park</Text>
            <Text tag={"golden_gate"}>Golden Gate</Text>
          </Picker>
        </VStack>

        <VStack alignment={"leading"} spacing={8}>
          <Text font={"headline"}>Map style</Text>
          <Picker
            title="Style"
            value={style}
            onChanged={(v: any) => setStyle(v as any)}
            pickerStyle={"segmented"}
          >
            <Text tag={"standard"}>standard</Text>
            <Text tag={"imagery"}>imagery</Text>
            <Text tag={"hybrid"}>hybrid</Text>
          </Picker>
        </VStack>

        <VStack alignment={"leading"} spacing={8}>
          <Text font={"headline"}>Appearance</Text>
          <Picker
            title="Appearance"
            value={appearance}
            onChanged={(v: any) => setAppearance(v as any)}
            pickerStyle={"segmented"}
          >
            <Text tag={"light"}>light</Text>
            <Text tag={"dark"}>dark</Text>
          </Picker>
        </VStack>

        <HStack spacing={8}>
          <Button title={loading ? "Rendering..." : "Take snapshot"} action={take} />
          {snap != null
            ? <Text font={"caption2"} foregroundStyle={"tertiaryLabel"}>
              {snap.size.width} × {snap.size.height} pt
            </Text>
            : null}
        </HStack>

        {err != null
          ? <Text font={"caption"} foregroundStyle={"systemRed"}>{err}</Text>
          : null}

        {snap != null
          ? <ZStack frame={{ width: SNAP_W, height: SNAP_H }}>
            <Image
              image={snap.image}
              resizable
              scaleToFit
              frame={{ width: SNAP_W, height: SNAP_H }}
              clipShape={{ type: "rect", cornerRadius: 12 }}
            />
            {/* 多个 POI 覆盖:每个 pin + 文字标签都按 snap.point(...) 投影到画布上。 */}
            {overlays.filter(o => o.inBounds).map(o =>
              <VStack key={o.name} spacing={1} position={{ x: o.pt.x, y: o.pt.y - 14 }}>
                <Image
                  systemName="mappin.circle.fill"
                  font={"title3"}
                  foregroundStyle={o.tint}
                />
                <Text
                  font={"caption2"}
                  foregroundStyle={"white"}
                  padding={{ horizontal: 4, vertical: 1 }}
                  background={"rgba(0, 0, 0, 0.55)"}
                  clipShape={{ type: "rect", cornerRadius: 4 }}
                >
                  {o.name}
                </Text>
              </VStack>
            )}
          </ZStack>
          : <Text font={"caption"} foregroundStyle={"tertiaryLabel"}>
            Snapshot will appear here after rendering.
          </Text>}

        {snap != null
          ? <Text font={"caption2"} foregroundStyle={"tertiaryLabel"}>
            {`${overlays.filter(o => o.inBounds).length} / ${overlays.length} POIs in bounds — out-of-bounds coordinates are clipped instead of bleeding past the image edge.`}
          </Text>
          : null}
      </VStack>
    </ScrollView>
  </NavigationStack>
}

async function run() {
  await Navigation.present({ element: <Example /> })
  Script.exit()
}

run()