From 8eb05161198810967d3652b339de08330bee1453 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Sun, 8 Mar 2026 16:37:26 -0400 Subject: [PATCH] feat: add wiki, fix README scoped package name, CI and package improvements - Add .wiki/ with Home, API-Reference, and Architecture pages - Add wiki-sync.yml workflow - Fix README: badge and install command now use @acamarata/qibla scope - Add Architecture and Documentation sections to README - Add README.md, CHANGELOG.md, LICENSE to files field - Update pack-check CI to verify README, CHANGELOG, LICENSE in tarball - Fix exports.import.types to ./dist/index.d.mts (matches tsup output) - Enable sourcemap: true in tsup config --- .github/workflows/ci.yml | 3 + .github/workflows/wiki-sync.yml | 38 +++++++ .wiki/API-Reference.md | 170 ++++++++++++++++++++++++++++++++ .wiki/Architecture.md | 116 ++++++++++++++++++++++ .wiki/Home.md | 55 +++++++++++ README.md | 29 ++++-- package.json | 5 +- tsup.config.ts | 2 +- 8 files changed, 409 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/wiki-sync.yml create mode 100644 .wiki/API-Reference.md create mode 100644 .wiki/Architecture.md create mode 100644 .wiki/Home.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56177a9..8a8bda1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,4 +79,7 @@ jobs: grep "dist/index.mjs" pack-output.txt grep "dist/index.d.ts" pack-output.txt grep "dist/index.d.mts" pack-output.txt + grep "README.md" pack-output.txt + grep "CHANGELOG.md" pack-output.txt + grep "LICENSE" pack-output.txt echo "Pack check passed" diff --git a/.github/workflows/wiki-sync.yml b/.github/workflows/wiki-sync.yml new file mode 100644 index 0000000..1cd4a85 --- /dev/null +++ b/.github/workflows/wiki-sync.yml @@ -0,0 +1,38 @@ +name: Sync Wiki + +on: + push: + branches: [main] + paths: ['.wiki/**'] + +permissions: + contents: write + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Checkout wiki + run: | + git clone "https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.wiki.git" .wiki-remote \ + || (mkdir -p .wiki-remote \ + && cd .wiki-remote \ + && git init \ + && git remote add origin "https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.wiki.git") + + - name: Sync wiki pages + run: | + cp .wiki/*.md .wiki-remote/ + cd .wiki-remote + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -A + if git diff --cached --quiet; then + echo "No wiki changes to commit" + else + git commit -m "Sync wiki from repo" + git push + fi diff --git a/.wiki/API-Reference.md b/.wiki/API-Reference.md new file mode 100644 index 0000000..ba95d7c --- /dev/null +++ b/.wiki/API-Reference.md @@ -0,0 +1,170 @@ +# API Reference + +## Functions + +### `qiblaAngle(lat, lng)` + +Computes the initial bearing from the given coordinates to the Ka'bah. + +Uses the forward azimuth formula from spherical trigonometry: + +``` +θ = atan2(sin(Δλ)·cos(φ₂), cos(φ₁)·sin(φ₂) − sin(φ₁)·cos(φ₂)·cos(Δλ)) +``` + +where φ₁, λ₁ is the observer and φ₂, λ₂ is the Ka'bah. + +**Parameters:** + +| Name | Type | Description | +| ----- | -------- | ------------------------------------------ | +| `lat` | `number` | Observer latitude in decimal degrees. Valid range: −90 to 90. | +| `lng` | `number` | Observer longitude in decimal degrees. Valid range: −180 to 180. | + +**Returns:** `number` — Bearing in degrees clockwise from true north. Range: [0, 360). + +**Throws:** `RangeError` if either coordinate is out of bounds. + +```typescript +import { qiblaAngle } from '@acamarata/qibla'; + +qiblaAngle(40.7128, -74.006); // ~58.48 (New York → Mecca) +qiblaAngle(51.5074, -0.1278); // ~119.0 (London → Mecca) +qiblaAngle(35.6762, 139.6503); // ~293.3 (Tokyo → Mecca) +``` + +--- + +### `compassDir(bearing)` + +Returns the eight-point compass abbreviation for a bearing. + +Maps the bearing to one of eight 45° sectors, selecting the nearest cardinal or intercardinal direction. + +**Parameters:** + +| Name | Type | Description | +| --------- | -------- | ---------------------------- | +| `bearing` | `number` | Bearing in degrees (0–360). | + +**Returns:** `CompassAbbr` — One of: `N`, `NE`, `E`, `SE`, `S`, `SW`, `W`, `NW`. + +```typescript +import { compassDir } from '@acamarata/qibla'; + +compassDir(0); // "N" +compassDir(45); // "NE" +compassDir(58.5); // "NE" +compassDir(270); // "W" +``` + +--- + +### `compassName(bearing)` + +Returns the full compass direction name for a bearing. + +**Parameters:** + +| Name | Type | Description | +| --------- | -------- | ---------------------------- | +| `bearing` | `number` | Bearing in degrees (0–360). | + +**Returns:** `CompassName` — One of: `North`, `Northeast`, `East`, `Southeast`, `South`, `Southwest`, `West`, `Northwest`. + +```typescript +import { compassName } from '@acamarata/qibla'; + +compassName(58.5); // "Northeast" +``` + +--- + +### `qiblaGreatCircle(lat, lng, steps?)` + +Generates waypoints along the great-circle geodesic from the observer to the Ka'bah, using the Slerp (spherical linear interpolation) formula. + +Useful for drawing the Qibla direction line on a map. Returns `steps + 1` points uniformly spaced along the geodesic. + +**Parameters:** + +| Name | Type | Default | Description | +| ------- | -------- | ------- | ------------------------------------------------ | +| `lat` | `number` | — | Observer latitude in decimal degrees (−90 to 90). | +| `lng` | `number` | — | Observer longitude in decimal degrees (−180 to 180). | +| `steps` | `number` | `120` | Number of segments. Result has `steps + 1` points. | + +**Returns:** `[number, number][]` — Array of `[latitude, longitude]` pairs in degrees. + +**Throws:** `RangeError` if coordinates are out of bounds. + +Special case: if the observer is at the Ka'bah (distance = 0), returns `[[lat, lng]]`. + +```typescript +import { qiblaGreatCircle } from '@acamarata/qibla'; + +const path = qiblaGreatCircle(40.7128, -74.006); // 121 points +// path[0] ≈ [40.7128, -74.006] (New York) +// path[120] ≈ [21.42, 39.83] (Ka'bah) +``` + +--- + +### `distanceKm(lat1, lng1, lat2, lng2)` + +Haversine distance between two points in kilometers. + +Uses the haversine formula with a spherical Earth (R = 6,371 km, WGS-84 volumetric mean). Accurate to within 0.5% globally. + +**Parameters:** + +| Name | Type | Description | +| ------ | -------- | ------------------------------------- | +| `lat1` | `number` | First point latitude in decimal degrees. | +| `lng1` | `number` | First point longitude in decimal degrees. | +| `lat2` | `number` | Second point latitude in decimal degrees. | +| `lng2` | `number` | Second point longitude in decimal degrees. | + +**Returns:** `number` — Distance in kilometers. + +```typescript +import { distanceKm, KAABA_LAT, KAABA_LNG } from '@acamarata/qibla'; + +distanceKm(40.7128, -74.006, KAABA_LAT, KAABA_LNG); // ~9,634 km +``` + +--- + +## Constants + +| Constant | Value | Description | +| ----------------- | ----------- | --------------------------------------------------- | +| `KAABA_LAT` | `21.422511` | Ka'bah center latitude in decimal degrees north. | +| `KAABA_LNG` | `39.82615` | Ka'bah center longitude in decimal degrees east. | +| `EARTH_RADIUS_KM` | `6371` | WGS-84 volumetric mean radius in kilometers. | + +```typescript +import { KAABA_LAT, KAABA_LNG, EARTH_RADIUS_KM } from '@acamarata/qibla'; +``` + +--- + +## Types + +### `CompassAbbr` + +```typescript +type CompassAbbr = 'N' | 'NE' | 'E' | 'SE' | 'S' | 'SW' | 'W' | 'NW'; +``` + +### `CompassName` + +```typescript +type CompassName = + | 'North' | 'Northeast' | 'East' | 'Southeast' + | 'South' | 'Southwest' | 'West' | 'Northwest'; +``` + +--- + +[Home](Home) | [Architecture](Architecture) diff --git a/.wiki/Architecture.md b/.wiki/Architecture.md new file mode 100644 index 0000000..bc86993 --- /dev/null +++ b/.wiki/Architecture.md @@ -0,0 +1,116 @@ +# Architecture + +## Overview + +`@acamarata/qibla` is a pure math library. No external dependencies. No I/O. All functions are synchronous and stateless. + +The source is a single TypeScript file (`src/index.ts`) compiled to dual CJS/ESM output via tsup. + +--- + +## Qibla Bearing: Forward Azimuth + +`qiblaAngle` uses the forward azimuth formula from spherical trigonometry, also called the "initial bearing" on the great circle. + +Given observer (φ₁, λ₁) and Ka'bah (φ₂, λ₂) in radians: + +``` +y = sin(λ₂ − λ₁) · cos(φ₂) +x = cos(φ₁) · sin(φ₂) − sin(φ₁) · cos(φ₂) · cos(λ₂ − λ₁) +θ = atan2(y, x) +``` + +`atan2` produces a result in (−π, π]. Adding 360° and taking modulo 360 converts to the [0, 360) convention, where 0° = North, 90° = East. + +This gives the bearing at the observer's location, not the arrival bearing at the Ka'bah. For a short trip within a city, the difference is negligible. For a trans-oceanic path, the bearing rotates continuously along the geodesic — `qiblaGreatCircle` shows this progression. + +--- + +## Great-Circle Path: Slerp + +`qiblaGreatCircle` uses the Slerp (spherical linear interpolation) formula to generate uniformly spaced waypoints along the geodesic. + +**Step 1 — Convert to 3D unit vectors** + +Lat/lng are converted to 3D Cartesian unit vectors on the unit sphere: + +``` +x = cos(φ) · cos(λ) +y = cos(φ) · sin(λ) +z = sin(φ) +``` + +**Step 2 — Compute the angular distance** + +The central angle d between the two points uses the formula: + +``` +d = 2 · asin( sqrt( sin²((φ₂−φ₁)/2) + cos(φ₁)·cos(φ₂)·sin²((λ₂−λ₁)/2) ) ) +``` + +This is equivalent to the haversine formula. If d = 0, the observer is at the Ka'bah — return immediately. + +**Step 3 — Interpolate** + +For each interpolation parameter f ∈ [0, 1]: + +``` +A = sin((1−f)·d) / sin(d) +B = sin(f·d) / sin(d) +P = A·P₁ + B·P₂ +``` + +where P₁ and P₂ are the 3D unit vectors. Convert the result back to lat/lng. + +This is numerically stable for all separations except d = 0 (handled separately) and d = π (antipodal points, undefined great circle). For practical use — observer and Ka'bah are never antipodal — this is not a concern. + +--- + +## Haversine Distance + +`distanceKm` implements the haversine formula: + +``` +a = sin²(Δφ/2) + cos(φ₁) · cos(φ₂) · sin²(Δλ/2) +c = 2 · atan2(√a, √(1−a)) +d = R · c +``` + +where R = 6,371 km (WGS-84 volumetric mean radius). + +The haversine formula is numerically stable for both small and large distances, unlike the simpler spherical law of cosines which loses precision for short arcs. + +--- + +## Compass Direction + +`compassDir` and `compassName` divide the 360° circle into eight 45° sectors. The sector index is: + +``` +index = round(bearing / 45) mod 8 +``` + +Rounding (not flooring) ensures each sector is centered on its cardinal/intercardinal direction: N covers 337.5–360° and 0–22.5°, NE covers 22.5–67.5°, and so on. + +--- + +## Ka'bah Coordinates + +The Ka'bah center is fixed at 21.422511°N, 39.82615°E. These coordinates come from verified GPS data and match the values used by major Islamic authority applications. The value is a constant — no runtime fetching. + +--- + +## Build + +``` +src/index.ts → tsup → dist/index.cjs (CommonJS) + → dist/index.mjs (ESM) + → dist/index.d.ts (type definitions, CJS fallback) + → dist/index.d.mts (type definitions, ESM) +``` + +tsup config uses `platform: 'neutral'` — the library has no Node.js-specific API calls and works identically in browsers, Deno, Bun, and all bundlers. + +--- + +[Home](Home) | [API Reference](API-Reference) diff --git a/.wiki/Home.md b/.wiki/Home.md new file mode 100644 index 0000000..d76c500 --- /dev/null +++ b/.wiki/Home.md @@ -0,0 +1,55 @@ +# @acamarata/qibla + +Qibla direction, great-circle path, and haversine distance. Pure math, zero dependencies. + +## Pages + +- [API Reference](API-Reference) — Full function and constant reference +- [Architecture](Architecture) — Algorithm design, spherical trigonometry, implementation decisions + +## What It Does + +This library computes three things: + +1. **Qibla bearing** — the initial compass bearing from any point on Earth to the Ka'bah in Mecca, using the forward azimuth formula from spherical trigonometry +2. **Great-circle path** — a series of waypoints along the geodesic from origin to Ka'bah, suitable for rendering on a map +3. **Haversine distance** — the surface distance between two coordinate pairs using the haversine formula + +All calculations use a spherical Earth model (WGS-84 volumetric mean radius, 6,371 km). The Ka'bah coordinates (21.422511°N, 39.82615°E) are sourced from verified GPS data. + +## Quick Start + +```bash +npm install @acamarata/qibla +``` + +```typescript +import { qiblaAngle, compassDir, distanceKm, KAABA_LAT, KAABA_LNG } from '@acamarata/qibla'; + +const bearing = qiblaAngle(40.7128, -74.006); // New York +console.log(bearing); // ~58.48 +console.log(compassDir(bearing)); // "NE" + +const km = distanceKm(40.7128, -74.006, KAABA_LAT, KAABA_LNG); +console.log(km); // ~9,634 +``` + +## Installation + +```bash +npm install @acamarata/qibla +# or +pnpm add @acamarata/qibla +# or +yarn add @acamarata/qibla +``` + +## Related Packages + +- [pray-calc](https://github.com/acamarata/pray-calc) — Islamic prayer times +- [nrel-spa](https://github.com/acamarata/nrel-spa) — NREL Solar Position Algorithm +- [moon-sighting](https://github.com/acamarata/moon-sighting) — Lunar crescent visibility + +--- + +[API Reference](API-Reference) | [Architecture](Architecture) diff --git a/README.md b/README.md index 323f9b5..20b1d2d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# qibla +# @acamarata/qibla -[![npm version](https://img.shields.io/npm/v/qibla.svg)](https://www.npmjs.com/package/qibla) +[![npm version](https://img.shields.io/npm/v/%40acamarata%2Fqibla.svg)](https://www.npmjs.com/package/%40acamarata%2Fqibla) [![CI](https://github.com/acamarata/qibla/actions/workflows/ci.yml/badge.svg)](https://github.com/acamarata/qibla/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) @@ -9,7 +9,7 @@ Qibla direction, great-circle path, and haversine distance. Pure math, zero depe ## Installation ```bash -npm install qibla +npm install @acamarata/qibla ``` ## Quick Start @@ -21,7 +21,7 @@ import { distanceKm, KAABA_LAT, KAABA_LNG, -} from "qibla"; +} from "@acamarata/qibla"; // Bearing from New York to the Ka'bah const bearing = qiblaAngle(40.7128, -74.006); @@ -73,6 +73,12 @@ Haversine distance between two points in kilometers (spherical Earth approximati | `KAABA_LNG` | 39.826150 | Ka'bah center longitude (degrees east) | | `EARTH_RADIUS_KM` | 6371 | WGS-84 volumetric mean radius | +## Architecture + +All calculations use the forward azimuth formula from spherical trigonometry. Great-circle paths use Slerp (spherical linear interpolation). Distance uses the haversine formula. The Ka'bah coordinates are fixed constants from verified GPS data. + +See [Architecture](https://github.com/acamarata/qibla/wiki/Architecture) for algorithm details. + ## Compatibility Node.js 20+. Works in browsers and all major bundlers (Webpack, Vite, Rollup, esbuild). Ships as dual CJS/ESM with full TypeScript definitions. @@ -80,15 +86,24 @@ Node.js 20+. Works in browsers and all major bundlers (Webpack, Vite, Rollup, es ## TypeScript ```typescript -import { qiblaAngle, CompassAbbr, CompassName } from "qibla"; +import { qiblaAngle, CompassAbbr, CompassName } from "@acamarata/qibla"; const bearing: number = qiblaAngle(40.7128, -74.006); ``` +## Documentation + +Full reference available on the [GitHub Wiki](https://github.com/acamarata/qibla/wiki): + +- [Home](https://github.com/acamarata/qibla/wiki/Home) — Overview and quick start +- [API Reference](https://github.com/acamarata/qibla/wiki/API-Reference) — Full function and constant reference +- [Architecture](https://github.com/acamarata/qibla/wiki/Architecture) — Algorithm design, spherical trigonometry, Slerp implementation + ## Related -- [pray-calc](https://github.com/acamarata/pray-calc) - Islamic prayer times calculator -- [nrel-spa](https://github.com/acamarata/nrel-spa) - NREL Solar Position Algorithm +- [pray-calc](https://github.com/acamarata/pray-calc) — Islamic prayer times calculator +- [nrel-spa](https://github.com/acamarata/nrel-spa) — NREL Solar Position Algorithm +- [moon-sighting](https://github.com/acamarata/moon-sighting) — Lunar crescent visibility ## License diff --git a/package.json b/package.json index 355d04b..688640b 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,10 @@ "dist/index.cjs", "dist/index.mjs", "dist/index.d.ts", - "dist/index.d.mts" + "dist/index.d.mts", + "README.md", + "CHANGELOG.md", + "LICENSE" ], "engines": { "node": ">=20" diff --git a/tsup.config.ts b/tsup.config.ts index 4da50cb..18087b5 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -7,7 +7,7 @@ export default defineConfig({ clean: true, outDir: "dist", splitting: false, - sourcemap: false, + sourcemap: true, target: "es2020", platform: "neutral", outExtension({ format }) {