{"version":3,"file":"projection.cjs","names":[],"sources":["../../src/geo/projection.ts"],"sourcesContent":["import { clampLatitude } from \"./types\";\nimport type { Coordinate, GeoBounds } from \"./types\";\n\n/**\n * A point projected onto the unit Web Mercator plane. Both axes are in `[0, 1]`\n * — `x` grows east, `y` grows south (screen convention).\n */\nexport interface MercatorPoint {\n    x: number;\n    y: number;\n}\n\n/** A pixel coordinate inside the plotting viewport. */\nexport interface PixelPoint {\n    x: number;\n    y: number;\n}\n\n/**\n * Web Mercator (EPSG:3857) latitude clamp. Latitudes beyond this diverge to\n * infinity in the projection, so tile maps cap here.\n */\nexport const MERCATOR_MAX_LATITUDE = 85.05112878;\n\n/**\n * Project a geographic coordinate onto the unit Web Mercator plane. This is the\n * same projection tile servers use, so a self-hosted tile layer and the\n * tile-free SVG plot line up pixel-for-pixel.\n *\n * @param coord - Coordinate to project.\n * @returns `{ x, y }` in `[0, 1]`.\n */\nexport function projectMercator(coord: Coordinate): MercatorPoint {\n    const lat = Math.max(\n        -MERCATOR_MAX_LATITUDE,\n        Math.min(MERCATOR_MAX_LATITUDE, clampLatitude(coord.latitude)),\n    );\n    const latRad = (lat * Math.PI) / 180;\n    const x = (coord.longitude + 180) / 360;\n    const y = (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2;\n    return { x, y };\n}\n\n/**\n * Inverse of {@link projectMercator}: recover a coordinate from a unit-plane\n * point.\n *\n * @param point - `{ x, y }` in `[0, 1]`.\n * @returns The geographic coordinate.\n */\nexport function unprojectMercator(point: MercatorPoint): Coordinate {\n    const longitude = point.x * 360 - 180;\n    const n = Math.PI * (1 - 2 * point.y);\n    const latitude = (Math.atan(Math.sinh(n)) * 180) / Math.PI;\n    return { latitude, longitude };\n}\n\n/** A ready-to-use mapping from coordinates to viewport pixels. */\nexport interface FittedProjection {\n    /** Project a coordinate to a pixel inside the viewport. */\n    project: (coord: Coordinate) => PixelPoint;\n    /** Uniform scale (unit-plane → pixels) actually used, after aspect fit. */\n    scale: number;\n    /** Viewport width in pixels. */\n    width: number;\n    /** Viewport height in pixels. */\n    height: number;\n}\n\n/** Options for {@link fitProjection}. */\nexport interface FitProjectionOptions {\n    /** Inner padding in pixels kept clear on every edge. Default: `16`. */\n    padding?: number;\n}\n\n/**\n * Build a projection that fits `bounds` into a `width × height` viewport while\n * preserving aspect ratio (uniform scale, centered). This is what powers the\n * tile-free trajectory plot: project the bounds, scale to the SVG box, keep\n * shapes undistorted.\n *\n * @param bounds - Geographic extent to fit.\n * @param width - Viewport width in pixels.\n * @param height - Viewport height in pixels.\n * @param options - Padding tuning.\n * @returns A {@link FittedProjection} with a `project(coord)` mapper.\n *\n * @tempest-limits param-count — `(bounds, width, height)` is the viewport the caller\n * measured, usually straight out of a `getBoundingClientRect()`, and the fourth\n * argument is already the options object. Public surface via the package root:\n * renaming these into one bag breaks callers and buys a count, not clarity.\n */\nexport function fitProjection(\n    bounds: GeoBounds,\n    width: number,\n    height: number,\n    options: FitProjectionOptions = {},\n): FittedProjection {\n    const { padding = 16 } = options;\n\n    const topLeft = projectMercator({\n        latitude: bounds.maxLatitude,\n        longitude: bounds.minLongitude,\n    });\n    const bottomRight = projectMercator({\n        latitude: bounds.minLatitude,\n        longitude: bounds.maxLongitude,\n    });\n\n    const spanX = bottomRight.x - topLeft.x || Number.EPSILON;\n    const spanY = bottomRight.y - topLeft.y || Number.EPSILON;\n\n    const innerWidth = Math.max(1, width - padding * 2);\n    const innerHeight = Math.max(1, height - padding * 2);\n\n    // Uniform scale keeps the trajectory undistorted; fit the tighter axis.\n    const scale = Math.min(innerWidth / spanX, innerHeight / spanY);\n\n    // Center the projected content within the padded box.\n    const offsetX = padding + (innerWidth - spanX * scale) / 2;\n    const offsetY = padding + (innerHeight - spanY * scale) / 2;\n\n    const project = (coord: Coordinate): PixelPoint => {\n        const projected = projectMercator(coord);\n        return {\n            x: offsetX + (projected.x - topLeft.x) * scale,\n            y: offsetY + (projected.y - topLeft.y) * scale,\n        };\n    };\n\n    return { project, scale, width, height };\n}\n"],"mappings":"+BAsBA,IAAa,EAAwB,YAUrC,SAAgB,EAAgB,EAAkC,CAK9D,IAAM,EAJM,KAAK,IACb,aACA,KAAK,IAAI,EAAuB,EAAA,cAAc,EAAM,QAAQ,CAAC,CAEjD,EAAM,KAAK,GAAM,IAGjC,MAAO,CAAE,GAFE,EAAM,UAAY,KAAO,IAExB,GADD,EAAI,KAAK,IAAI,KAAK,IAAI,CAAM,EAAI,EAAI,KAAK,IAAI,CAAM,CAAC,EAAI,KAAK,IAAM,CAChE,CAClB,CASA,SAAgB,EAAkB,EAAkC,CAChE,IAAM,EAAY,EAAM,EAAI,IAAM,IAC5B,EAAI,KAAK,IAAM,EAAI,EAAI,EAAM,GAEnC,MAAO,CAAE,SADS,KAAK,KAAK,KAAK,KAAK,CAAC,CAAC,EAAI,IAAO,KAAK,GACrC,WAAU,CACjC,CAqCA,SAAgB,EACZ,EACA,EACA,EACA,EAAgC,CAAC,EACjB,CAChB,GAAM,CAAE,UAAU,IAAO,EAEnB,EAAU,EAAgB,CAC5B,SAAU,EAAO,YACjB,UAAW,EAAO,YACtB,CAAC,EACK,EAAc,EAAgB,CAChC,SAAU,EAAO,YACjB,UAAW,EAAO,YACtB,CAAC,EAEK,EAAQ,EAAY,EAAI,EAAQ,UAChC,EAAQ,EAAY,EAAI,EAAQ,UAEhC,EAAa,KAAK,IAAI,EAAG,EAAQ,EAAU,CAAC,EAC5C,EAAc,KAAK,IAAI,EAAG,EAAS,EAAU,CAAC,EAG9C,EAAQ,KAAK,IAAI,EAAa,EAAO,EAAc,CAAK,EAGxD,EAAU,GAAW,EAAa,EAAQ,GAAS,EACnD,EAAU,GAAW,EAAc,EAAQ,GAAS,EAU1D,MAAO,CAAE,QARQ,GAAkC,CAC/C,IAAM,EAAY,EAAgB,CAAK,EACvC,MAAO,CACH,EAAG,GAAW,EAAU,EAAI,EAAQ,GAAK,EACzC,EAAG,GAAW,EAAU,EAAI,EAAQ,GAAK,CAC7C,CACJ,EAEkB,QAAO,QAAO,QAAO,CAC3C"}