{"version":3,"file":"geo_basics.mjs","names":[],"sources":["../../../src/batteries/tools/geo_basics/index.ts"],"sourcesContent":["/**\n * Pre-constructed tools for basic geographic distance and coordinate calculations.\n *\n * @module @nhtio/adk/batteries/tools/geo_basics\n *\n * @remarks\n * Pre-constructed bundled tools for the `geo_basics` category. Import individually, the whole\n * category, or import every tool via `@nhtio/adk/batteries`.\n */\n\nimport { Tool } from '@nhtio/adk/common'\nimport { validator } from '@nhtio/validation'\n\nconst EARTH_RADIUS_KM = 6371.0088\n\nfunction toRad(deg: number): number {\n  return (deg * Math.PI) / 180\n}\n\nfunction haversineKm(lat1: number, lon1: number, lat2: number, lon2: number): number {\n  const dLat = toRad(lat2 - lat1)\n  const dLon = toRad(lon2 - lon1)\n  const a =\n    Math.sin(dLat / 2) ** 2 +\n    Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2\n  return 2 * EARTH_RADIUS_KM * Math.asin(Math.sqrt(a))\n}\n\nfunction validateLatLon(lat: unknown, lon: unknown, label: string): string | null {\n  if (typeof lat !== 'number' || !Number.isFinite(lat) || lat < -90 || lat > 90)\n    return `Error: ${label} latitude must be a number between -90 and 90.`\n  if (typeof lon !== 'number' || !Number.isFinite(lon) || lon < -180 || lon > 180)\n    return `Error: ${label} longitude must be a number between -180 and 180.`\n  return null\n}\n\n/**\n * Great-circle (haversine) distance between two geographic coordinates.\n *\n * @remarks\n * Uses the Earth's mean radius (6371.0088 km). Returns both kilometres and miles.\n */\nexport const geoDistanceTool = new Tool({\n  name: 'geo_distance',\n  description:\n    'Calculate the great-circle (haversine) distance between two geographic coordinates. Returns distance in km and miles.',\n  inputSchema: validator.object({\n    lat1: validator\n      .number()\n      .required()\n      .description('Latitude of point A (decimal degrees, -90 to 90)'),\n    lon1: validator\n      .number()\n      .required()\n      .description('Longitude of point A (decimal degrees, -180 to 180)'),\n    lat2: validator\n      .number()\n      .required()\n      .description('Latitude of point B (decimal degrees, -90 to 90)'),\n    lon2: validator\n      .number()\n      .required()\n      .description('Longitude of point B (decimal degrees, -180 to 180)'),\n  }),\n  handler: async (args) => {\n    const { lat1, lon1, lat2, lon2 } = args as {\n      lat1: number\n      lon1: number\n      lat2: number\n      lon2: number\n    }\n\n    const errA = validateLatLon(lat1, lon1, 'point A')\n    if (errA) return errA\n    const errB = validateLatLon(lat2, lon2, 'point B')\n    if (errB) return errB\n\n    const km = haversineKm(lat1, lon1, lat2, lon2)\n    const miles = km * 0.621371\n\n    return `Distance: ${km.toFixed(4)} km / ${miles.toFixed(4)} miles`\n  },\n})\n\n/**\n * Check whether a point lies within a radius of a centre coordinate.\n *\n * @remarks\n * Uses the haversine distance internally; returns the radius unit chosen by the caller\n * (km or miles) in the result string.\n */\nexport const geoWithinRadiusTool = new Tool({\n  name: 'geo_within_radius',\n  description:\n    'Check whether a point is within a given radius (km or miles) of a center coordinate.',\n  inputSchema: validator.object({\n    center_lat: validator.number().required().description('Center latitude'),\n    center_lon: validator.number().required().description('Center longitude'),\n    point_lat: validator.number().required().description('Point latitude to test'),\n    point_lon: validator.number().required().description('Point longitude to test'),\n    radius: validator.number().required().description('Radius value (must be positive)'),\n    unit: validator\n      .string()\n      .valid('km', 'miles')\n      .default('km')\n      .description('Unit for the radius (default: km)'),\n  }),\n  handler: async (args) => {\n    const {\n      center_lat: centerLat,\n      center_lon: centerLon,\n      point_lat: pointLat,\n      point_lon: pointLon,\n      radius,\n      unit,\n    } = args as {\n      center_lat: number\n      center_lon: number\n      point_lat: number\n      point_lon: number\n      radius: number\n      unit: string\n    }\n\n    const errC = validateLatLon(centerLat, centerLon, 'center')\n    if (errC) return errC\n    const errP = validateLatLon(pointLat, pointLon, 'point')\n    if (errP) return errP\n    if (!Number.isFinite(radius) || radius <= 0) return 'Error: radius must be a positive number.'\n\n    const km = haversineKm(centerLat, centerLon, pointLat, pointLon)\n    const distInUnit = unit === 'miles' ? km * 0.621371 : km\n    const within = distInUnit <= radius\n\n    return `${within ? 'Yes' : 'No'} — distance is ${distInUnit.toFixed(4)} ${unit} (radius: ${radius} ${unit})`\n  },\n})\n\n/**\n * Check whether a point falls inside an axis-aligned bounding box.\n *\n * @remarks\n * The bounding box is defined by its SW and NE corners. Boxes that wrap the antimeridian\n * (i.e. SW longitude greater than NE longitude) are handled correctly.\n */\nexport const geoBboxContainsTool = new Tool({\n  name: 'geo_bbox_contains',\n  description:\n    'Check whether a point falls inside an axis-aligned bounding box defined by its SW and NE corners.',\n  inputSchema: validator.object({\n    sw_lat: validator.number().required().description('South-west corner latitude'),\n    sw_lon: validator.number().required().description('South-west corner longitude'),\n    ne_lat: validator.number().required().description('North-east corner latitude'),\n    ne_lon: validator.number().required().description('North-east corner longitude'),\n    point_lat: validator.number().required().description('Point latitude to test'),\n    point_lon: validator.number().required().description('Point longitude to test'),\n  }),\n  handler: async (args) => {\n    const {\n      sw_lat: swLat,\n      sw_lon: swLon,\n      ne_lat: neLat,\n      ne_lon: neLon,\n      point_lat: pointLat,\n      point_lon: pointLon,\n    } = args as {\n      sw_lat: number\n      sw_lon: number\n      ne_lat: number\n      ne_lon: number\n      point_lat: number\n      point_lon: number\n    }\n\n    for (const [lat, lon, label] of [\n      [swLat, swLon, 'SW'],\n      [neLat, neLon, 'NE'],\n      [pointLat, pointLon, 'point'],\n    ] as [number, number, string][]) {\n      const err = validateLatLon(lat, lon, label)\n      if (err) return err\n    }\n\n    if (swLat > neLat) return 'Error: SW latitude must be ≤ NE latitude.'\n\n    const latIn = pointLat >= swLat && pointLat <= neLat\n\n    // Handle bounding boxes that wrap the antimeridian\n    const lonIn =\n      swLon <= neLon\n        ? pointLon >= swLon && pointLon <= neLon\n        : pointLon >= swLon || pointLon <= neLon\n\n    const inside = latIn && lonIn\n    return inside\n      ? `Yes — point (${pointLat}, ${pointLon}) is inside the bounding box.`\n      : `No — point (${pointLat}, ${pointLon}) is outside the bounding box.`\n  },\n})\n"],"mappings":"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"}