Compare commits

...

15 commits
v1.1.1 ... main

Author SHA1 Message Date
Aric Camarata
1cbfa04acf
add opt-in anonymous telemetry (#1)
Some checks failed
CI / Test (Node 20) (push) Failing after 34s
CI / Test (Node 22) (push) Failing after 35s
CI / Test (Node 24) (push) Failing after 26s
CI / Lint & Format (push) Failing after 27s
CI / Typecheck (push) Failing after 25s
CI / Pack Check (push) Failing after 31s
CI / Coverage (push) Failing after 3s
* add opt-in telemetry via @acamarata/telemetry (off by default)

* chore: update lockfile for @acamarata/telemetry devDep

* chore: fix prettier formatting on telemetry import
2026-06-30 15:56:57 -04:00
Aric Camarata
b5c6ca007a build: use prepack hook so npm pack/publish reliably emit index.d.mts 2026-06-13 10:30:54 -04:00
Aric Camarata
64bb9a91d0 chore: stop tracking generated coverage artifacts 2026-06-13 10:27:10 -04:00
Aric Camarata
88d7dacaf2 ci: fix eslint parser devDeps and typed-linting config 2026-05-31 08:47:15 -04:00
Aric Camarata
ac4e7e6a8a chore: bump to v1.1.2 2026-05-30 19:11:44 -04:00
Aric Camarata
ee2cf4d92e chore: update docs and CI (E8 QA + E11 consolidation prep) 2026-05-30 18:38:21 -04:00
Aric Camarata
ee782babdb docs: refresh TypeDoc API output (T-E8-03 QA-A verify) 2026-05-30 17:48:46 -04:00
Aric Camarata
a59756bd07 docs: add TypeDoc API generation (typedoc@0.28.19 + typedoc-plugin-markdown@4.11.0)
Add typedoc and typedoc-plugin-markdown as devDependencies. Add typedoc.json config
targeting src/index.ts with markdown output to .github/wiki/api. Add docs script to
package.json. Generate initial API reference pages.

Part of T-E8-03 — TypeDoc automation for all 12 JS/TS packages.
2026-05-30 16:41:58 -04:00
Aric Camarata
1546cd5c71 chore: adopt shared config packages (tsconfig, eslint, prettier) 2026-05-30 15:07:02 -04:00
Aric Camarata
d0a04896b9 ci: corepack before setup-node, scope prettier to src/, emit d.mts 2026-05-29 20:05:31 -04:00
Aric Camarata
df2cc65aed ci(qibla): graceful wiki-sync skip when wiki uninitialized (no red CI) 2026-05-29 15:21:02 -04:00
Aric Camarata
0f62a0e904 ci(qibla): self-healing wiki-sync (bootstrap empty wiki, match other repos) 2026-05-29 15:09:20 -04:00
Aric Camarata
72ffd3659b chore: untrack AGENTS.md (AI working memory, not source code) 2026-05-29 06:36:41 -04:00
Aric Camarata
2236cc0b2d docs: add quickstart, advanced guide, and examples for qibla 2026-05-28 14:14:17 -04:00
Aric Camarata
53416d512a docs(e6): add wiki pages — Sidebar, Footer, Contributing, SECURITY, CODE_OF_CONDUCT 2026-05-28 14:00:04 -04:00
38 changed files with 2070 additions and 1366 deletions

View file

@ -1,57 +0,0 @@
# qibla — PRI (Per-Repo Instructions)
**Cascade:** GCI → ASI → PPI (`/Volumes/X9/Sites/acamarata/.claude/CLAUDE.md`) → **PRI (this file)**
## Repo Overview
**Package:** `@acamarata/qibla@1.0.0`
**Registry:** npm (public, `access: public`)
**Scoped name:** `@acamarata/qibla` — note the `@acamarata/` scope prefix in all install/publish commands
**Language:** TypeScript
**Runtime deps:** zero — pure math, no external dependencies
**Build:** tsup, dual CJS (`index.cjs`) + ESM (`index.mjs`) output
**Dart counterpart:** `qibla@1.0.0` on pub.dev (publisher: ariccamarata.com), repo: `qibla-dart`
## What It Does
Qibla direction, great-circle path, and haversine distance toward the Ka'bah (Mecca).
Exported functions:
- `qiblaAngle(lat, lng)` — initial bearing to Ka'bah, clockwise from north (0-360)
- `compassDir(bearing)` — 8-point compass abbreviation (N, NE, E, SE, S, SW, W, NW)
- `compassName(bearing)` — full compass name (North, Northeast, etc.)
- `qiblaGreatCircle(lat, lng, steps?)` — Slerp waypoints along the great-circle path to Ka'bah
- `distanceKm(lat1, lng1, lat2, lng2)` — haversine distance in km
Exported constants:
- `KAABA_LAT = 21.422511`
- `KAABA_LNG = 39.826150`
- `EARTH_RADIUS_KM = 6371`
## Project Rules (inherits from acamarata PPI)
This repo follows the full acamarata npm package standard. Key points:
- pnpm only — `pnpm install`, `pnpm test`, `pnpm run build`
- No AI attribution anywhere in tracked files
- Writing quality: no em dashes as connectors, no AI tells, academic technical tone
- Publishing requires explicit user approval
- Version bumps require CHANGELOG.md update first
## Dart Counterpart Relationship
The JS and Dart packages implement the same algorithm. Keep them in sync on:
- Ka'bah coordinate constants (KAABA_LAT / KAABA_LNG)
- Algorithm correctness (forward azimuth formula, haversine, Slerp)
- API surface parity (functions and constants match across both)
When updating the JS package in a way that affects algorithm or constants, note whether the Dart package (`qibla-dart`) needs the same fix.
## npm Publish Command
```bash
npm publish --access public
```
The `@acamarata/` scope requires `--access public` on first publish. Already set in `publishConfig` but include it explicitly to avoid accidental private publish.

34
.github/wiki/CODE_OF_CONDUCT.md vendored Normal file
View file

@ -0,0 +1,34 @@
# Code of Conduct
## Summary
Be direct, be respectful, and focus on the work.
## Standards
Constructive behavior:
- Technical criticism aimed at code and ideas, not people
- Clear and specific feedback with examples where possible
- Acknowledging when you are wrong or do not know something
- Staying on topic in issues and pull requests
Unacceptable behavior:
- Personal attacks, insults, or harassment
- Sustained off-topic disruption
- Publishing private information without consent
## Scope
This applies to all project spaces: GitHub issues, pull requests, discussions, and any other venue where project work happens.
## Enforcement
The project maintainer handles violations. Contact: aric.camarata@gmail.com.
Reports are reviewed promptly. Responses range from a private note to a permanent ban, depending on severity and history.
## Attribution
This code of conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1.

48
.github/wiki/Contributing.md vendored Normal file
View file

@ -0,0 +1,48 @@
# Contributing
## Prerequisites
- Node.js 20 or later
- pnpm (enabled via corepack: `corepack enable`)
## Setup
```sh
git clone https://github.com/acamarata/qibla.git
cd qibla
pnpm install
```
## Development
```sh
pnpm build # compile TypeScript
pnpm test # build + run test suite
pnpm run typecheck # type-check without emitting
pnpm run lint # ESLint
pnpm run format # Prettier format
```
## Project Structure
```
src/
index.ts all exports (qiblaAngle, compassDir, compassName, qiblaGreatCircle, distanceKm)
types.ts TypeScript types
dist/ tsup build output (gitignored)
test.mjs ESM test suite
test-cjs.cjs CJS test subset
```
## Ka'bah Coordinates
The Ka'bah position is defined in `src/index.ts` as constants `KAABA_LAT` and `KAABA_LNG`. These are sourced from high-precision geodetic measurements. Do not change them without a reference.
This package has a Dart counterpart (`qibla` on pub.dev). If you update the Ka'bah coordinates or the algorithm, the Dart package should receive the same update.
## Pull Requests
- One logical change per PR
- Include tests covering the new behavior
- Update `CHANGELOG.md` under `[Unreleased]`
- Do not bump the version number

26
.github/wiki/SECURITY.md vendored Normal file
View file

@ -0,0 +1,26 @@
# Security Policy
## Supported Versions
| Version | Supported |
| --- | --- |
| 1.x | Yes |
## Reporting a Vulnerability
Do not open a public GitHub issue for security vulnerabilities.
Email: aric.camarata@gmail.com
Include:
- A description of the vulnerability
- Steps to reproduce
- Potential impact
- Any suggested fix, if you have one
You will receive an acknowledgment within 48 hours and a resolution timeline within 7 days.
## Scope
This package is a pure math library. It performs no network requests, reads no files, and holds no credentials. All computations are deterministic spherical geometry. The primary security concern would be a supply-chain compromise of the npm package.

1
.github/wiki/_Footer.md vendored Normal file
View file

@ -0,0 +1 @@
[npm](https://www.npmjs.com/package/@acamarata/qibla) · [GitHub](https://github.com/acamarata/qibla) · [Changelog](https://github.com/acamarata/qibla/blob/main/CHANGELOG.md) · MIT License

37
.github/wiki/_Sidebar.md vendored Normal file
View file

@ -0,0 +1,37 @@
## @acamarata/qibla
**[Home](Home)**
**Guides**
- [Quick Start](guides/quickstart)
- [Advanced](guides/advanced)
**Reference**
- [API Reference](API-Reference)
- [Architecture](Architecture)
- [Benchmarks](benchmarks/index)
**API — Functions**
- [qiblaAngle](api/qiblaAngle)
- [compassDir](api/compassDir)
- [compassName](api/compassName)
- [qiblaGreatCircle](api/qiblaGreatCircle)
- [distanceKm](api/distanceKm)
**API — Constants & Types**
- [Constants](api/constants)
- [Types](api/types)
**Examples**
- [Qibla lookup](examples/qibla-lookup)
- [Great-circle path](examples/great-circle-path)
**Contributing**
- [Contributing](CONTRIBUTING)
- [Code of Conduct](CODE_OF_CONDUCT)
- [Security](SECURITY)
**Links**
- [npm](https://www.npmjs.com/package/@acamarata/qibla)
- [GitHub](https://github.com/acamarata/qibla)
- [Changelog](https://github.com/acamarata/qibla/blob/main/CHANGELOG.md)

36
.github/wiki/api/README.md vendored Normal file
View file

@ -0,0 +1,36 @@
**@acamarata/qibla v1.1.1**
***
# @acamarata/qibla v1.1.1
Qibla direction utilities. Pure math, zero external dependencies.
Computes the initial bearing (forward azimuth) from any point on Earth to
the Ka'bah using the spherical law of cosines. Includes compass direction
lookup, great-circle interpolation, and haversine distance.
Ka'bah coordinates sourced from verified GPS data.
SPORT: packages.md — @acamarata/qibla row
## Type Aliases
- [CompassAbbr](type-aliases/CompassAbbr.md)
- [CompassName](type-aliases/CompassName.md)
## Variables
- [COMPASS\_ABBR](variables/COMPASS_ABBR.md)
- [COMPASS\_NAMES](variables/COMPASS_NAMES.md)
- [EARTH\_RADIUS\_KM](variables/EARTH_RADIUS_KM.md)
- [KAABA\_LAT](variables/KAABA_LAT.md)
- [KAABA\_LNG](variables/KAABA_LNG.md)
## Functions
- [compassDir](functions/compassDir.md)
- [compassName](functions/compassName.md)
- [distanceKm](functions/distanceKm.md)
- [qiblaAngle](functions/qiblaAngle.md)
- [qiblaGreatCircle](functions/qiblaGreatCircle.md)

View file

@ -0,0 +1,27 @@
[**@acamarata/qibla v1.1.1**](../README.md)
***
[@acamarata/qibla](../README.md) / compassDir
# Function: compassDir()
> **compassDir**(`bearing`): `"N"` \| `"NE"` \| `"E"` \| `"SE"` \| `"S"` \| `"SW"` \| `"W"` \| `"NW"`
Defined in: [index.ts:68](https://github.com/acamarata/qibla/blob/a59756bd074a18a3c9cea4311d135cfb23a5ec7d/src/index.ts#L68)
Eight-point compass abbreviation for a bearing.
## Parameters
### bearing
`number`
Bearing in degrees (0-360).
## Returns
`"N"` \| `"NE"` \| `"E"` \| `"SE"` \| `"S"` \| `"SW"` \| `"W"` \| `"NW"`
Two-letter compass abbreviation (N, NE, E, SE, S, SW, W, NW).

View file

@ -0,0 +1,27 @@
[**@acamarata/qibla v1.1.1**](../README.md)
***
[@acamarata/qibla](../README.md) / compassName
# Function: compassName()
> **compassName**(`bearing`): `"North"` \| `"Northeast"` \| `"East"` \| `"Southeast"` \| `"South"` \| `"Southwest"` \| `"West"` \| `"Northwest"`
Defined in: [index.ts:80](https://github.com/acamarata/qibla/blob/a59756bd074a18a3c9cea4311d135cfb23a5ec7d/src/index.ts#L80)
Full compass direction name for a bearing.
## Parameters
### bearing
`number`
Bearing in degrees (0-360).
## Returns
`"North"` \| `"Northeast"` \| `"East"` \| `"Southeast"` \| `"South"` \| `"Southwest"` \| `"West"` \| `"Northwest"`
Full direction name (North, Northeast, etc.).

View file

@ -0,0 +1,45 @@
[**@acamarata/qibla v1.1.1**](../README.md)
***
[@acamarata/qibla](../README.md) / distanceKm
# Function: distanceKm()
> **distanceKm**(`lat1`, `lng1`, `lat2`, `lng2`): `number`
Defined in: [index.ts:142](https://github.com/acamarata/qibla/blob/a59756bd074a18a3c9cea4311d135cfb23a5ec7d/src/index.ts#L142)
Haversine distance between two coordinate pairs.
## Parameters
### 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 (spherical Earth approximation).

View file

@ -0,0 +1,51 @@
[**@acamarata/qibla v1.1.1**](../README.md)
***
[@acamarata/qibla](../README.md) / qiblaAngle
# Function: qiblaAngle()
> **qiblaAngle**(`lat`, `lng`): `number`
Defined in: [index.ts:46](https://github.com/acamarata/qibla/blob/a59756bd074a18a3c9cea4311d135cfb23a5ec7d/src/index.ts#L46)
Qibla bearing in degrees clockwise from true north.
Uses the forward azimuth formula from spherical trigonometry.
Result range: [0, 360).
## Parameters
### lat
`number`
Observer latitude in decimal degrees (-90 to 90).
### lng
`number`
Observer longitude in decimal degrees (-180 to 180).
## Returns
`number`
Bearing in degrees clockwise from north (0 = N, 90 = E, 180 = S, 270 = W).
## Throws
If latitude is outside [-90, 90] or longitude outside [-180, 180].
## Example
```ts
qiblaAngle(40.7128, -74.006); // ~58.49 (New York)
qiblaAngle(51.5074, -0.1278); // ~119.0 (London)
```
## See
[https://github.com/acamarata/qibla/wiki/api/qiblaAngle](https://github.com/acamarata/qibla/wiki/api/qiblaAngle) Wiki API page

View file

@ -0,0 +1,46 @@
[**@acamarata/qibla v1.1.1**](../README.md)
***
[@acamarata/qibla](../README.md) / qiblaGreatCircle
# Function: qiblaGreatCircle()
> **qiblaGreatCircle**(`lat`, `lng`, `steps?`): \[`number`, `number`\][]
Defined in: [index.ts:98](https://github.com/acamarata/qibla/blob/a59756bd074a18a3c9cea4311d135cfb23a5ec7d/src/index.ts#L98)
Great-circle waypoints from [lat, lng] to the Ka'bah.
Uses the Slerp (spherical linear interpolation) formula. Useful for
drawing Qibla direction lines on maps.
## Parameters
### lat
`number`
Origin latitude in decimal degrees.
### lng
`number`
Origin longitude in decimal degrees.
### steps?
`number` = `120`
Number of segments (default: 120, producing 121 points).
## Returns
\[`number`, `number`\][]
Array of [latitude, longitude] pairs in degrees.
## Throws
If latitude is outside [-90, 90] or longitude outside [-180, 180].

View file

@ -0,0 +1,13 @@
[**@acamarata/qibla v1.1.1**](../README.md)
***
[@acamarata/qibla](../README.md) / CompassAbbr
# Type Alias: CompassAbbr
> **CompassAbbr** = *typeof* [`COMPASS_ABBR`](../variables/COMPASS_ABBR.md)\[`number`\]
Defined in: [types.ts:26](https://github.com/acamarata/qibla/blob/a59756bd074a18a3c9cea4311d135cfb23a5ec7d/src/types.ts#L26)
Compass abbreviation type.

View file

@ -0,0 +1,13 @@
[**@acamarata/qibla v1.1.1**](../README.md)
***
[@acamarata/qibla](../README.md) / CompassName
# Type Alias: CompassName
> **CompassName** = *typeof* [`COMPASS_NAMES`](../variables/COMPASS_NAMES.md)\[`number`\]
Defined in: [types.ts:29](https://github.com/acamarata/qibla/blob/a59756bd074a18a3c9cea4311d135cfb23a5ec7d/src/types.ts#L29)
Compass full name type.

View file

@ -0,0 +1,13 @@
[**@acamarata/qibla v1.1.1**](../README.md)
***
[@acamarata/qibla](../README.md) / COMPASS\_ABBR
# Variable: COMPASS\_ABBR
> `const` **COMPASS\_ABBR**: readonly \[`"N"`, `"NE"`, `"E"`, `"SE"`, `"S"`, `"SW"`, `"W"`, `"NW"`\]
Defined in: [types.ts:11](https://github.com/acamarata/qibla/blob/a59756bd074a18a3c9cea4311d135cfb23a5ec7d/src/types.ts#L11)
Eight-point compass abbreviations.

View file

@ -0,0 +1,13 @@
[**@acamarata/qibla v1.1.1**](../README.md)
***
[@acamarata/qibla](../README.md) / COMPASS\_NAMES
# Variable: COMPASS\_NAMES
> `const` **COMPASS\_NAMES**: readonly \[`"North"`, `"Northeast"`, `"East"`, `"Southeast"`, `"South"`, `"Southwest"`, `"West"`, `"Northwest"`\]
Defined in: [types.ts:14](https://github.com/acamarata/qibla/blob/a59756bd074a18a3c9cea4311d135cfb23a5ec7d/src/types.ts#L14)
Eight-point compass full names.

View file

@ -0,0 +1,13 @@
[**@acamarata/qibla v1.1.1**](../README.md)
***
[@acamarata/qibla](../README.md) / EARTH\_RADIUS\_KM
# Variable: EARTH\_RADIUS\_KM
> `const` **EARTH\_RADIUS\_KM**: `6371` = `6371`
Defined in: [types.ts:8](https://github.com/acamarata/qibla/blob/a59756bd074a18a3c9cea4311d135cfb23a5ec7d/src/types.ts#L8)
Mean radius of the Earth in kilometers (WGS-84 volumetric mean).

13
.github/wiki/api/variables/KAABA_LAT.md vendored Normal file
View file

@ -0,0 +1,13 @@
[**@acamarata/qibla v1.1.1**](../README.md)
***
[@acamarata/qibla](../README.md) / KAABA\_LAT
# Variable: KAABA\_LAT
> `const` **KAABA\_LAT**: `21.422511` = `21.422511`
Defined in: [types.ts:2](https://github.com/acamarata/qibla/blob/a59756bd074a18a3c9cea4311d135cfb23a5ec7d/src/types.ts#L2)
Latitude of the Ka'bah center, Masjid al-Haram, Mecca (degrees north).

13
.github/wiki/api/variables/KAABA_LNG.md vendored Normal file
View file

@ -0,0 +1,13 @@
[**@acamarata/qibla v1.1.1**](../README.md)
***
[@acamarata/qibla](../README.md) / KAABA\_LNG
# Variable: KAABA\_LNG
> `const` **KAABA\_LNG**: `39.82615` = `39.82615`
Defined in: [types.ts:5](https://github.com/acamarata/qibla/blob/a59756bd074a18a3c9cea4311d135cfb23a5ec7d/src/types.ts#L5)
Longitude of the Ka'bah center, Masjid al-Haram, Mecca (degrees east).

57
.github/wiki/benchmarks/index.md vendored Normal file
View file

@ -0,0 +1,57 @@
# Benchmarks
Measured on Apple M-series hardware, Node.js v25, using 1,000,000 iterations with a 1,000-call warm-up.
## Bundle size
| Format | Minified | Gzipped |
| ------ | -------- | ------- |
| ESM (`index.mjs`) | 2.7 KB | 949 B |
| CJS (`index.cjs`) | 3.8 KB | 1.3 KB |
The library has zero external dependencies. Both formats are shipped in the npm package.
## Throughput
| Operation | Ops / sec |
| --------- | --------- |
| `qiblaAngle` (forward azimuth) | ~180,000,000 |
| `distanceKm` (haversine) | ~407,000,000 |
These numbers reflect the V8 JIT at peak — real applications with diverse inputs and cold starts will see lower throughput. The point is that neither function is a bottleneck: both run in nanoseconds per call.
## Reproducing the benchmark
```js
import { qiblaAngle, distanceKm, KAABA_LAT, KAABA_LNG } from '@acamarata/qibla';
const N = 1_000_000;
const LATS = Array.from({ length: 100 }, (_, i) => (i - 50) * 1.8);
const LNGS = Array.from({ length: 100 }, (_, i) => (i - 50) * 3.6);
// Warm up
for (let i = 0; i < 1000; i++) qiblaAngle(LATS[i % 100], LNGS[i % 100]);
// qiblaAngle
const t0 = performance.now();
for (let i = 0; i < N; i++) qiblaAngle(LATS[i % 100], LNGS[i % 100]);
const t1 = performance.now();
// distanceKm
const t2 = performance.now();
for (let i = 0; i < N; i++) distanceKm(LATS[i % 100], LNGS[i % 100], KAABA_LAT, KAABA_LNG);
const t3 = performance.now();
console.log(`qiblaAngle: ${Math.round(N / ((t1 - t0) / 1000)).toLocaleString()} ops/s`);
console.log(`distanceKm: ${Math.round(N / ((t3 - t2) / 1000)).toLocaleString()} ops/s`);
```
## Notes
- V8's JIT optimizer eliminates much of the trig overhead at peak throughput. Expect 40-60% of these values in production with cold module loads.
- The library works identically in browsers (V8/SpiderMonkey/WebKit), Deno, and Bun. Performance will vary by runtime and optimization tier.
- `qiblaGreatCircle` is not benchmarked here because throughput depends on the step count. At the default of 120 steps, it runs in roughly 2-5 microseconds per call.
---
[Home](../Home) | [API Reference](../API-Reference)

View file

@ -0,0 +1,56 @@
# Example: Great-Circle Path to Mecca
Generate waypoints along the great-circle path from a city to the Ka'bah.
```js
import { qiblaAngle, qiblaGreatCircle, distanceKm, KAABA_LAT, KAABA_LNG } from '@acamarata/qibla';
const ORIGIN_NAME = 'New York';
const ORIGIN_LAT = 40.7128;
const ORIGIN_LNG = -74.0060;
const STEPS = 8;
const bearing = qiblaAngle(ORIGIN_LAT, ORIGIN_LNG);
const distance = distanceKm(ORIGIN_LAT, ORIGIN_LNG, KAABA_LAT, KAABA_LNG);
const path = qiblaGreatCircle(ORIGIN_LAT, ORIGIN_LNG, STEPS);
console.log(`Great-circle path: ${ORIGIN_NAME} → Mecca`);
console.log(` Initial bearing: ${bearing.toFixed(2)}°`);
console.log(` Total distance: ${Math.round(distance).toLocaleString()} km`);
console.log(` Waypoints (${STEPS}):`);
console.log('');
const stepKm = distance / (STEPS - 1);
for (let i = 0; i < path.length; i++) {
const [lat, lng] = path[i];
const km = Math.round(stepKm * i);
const tag = i === 0 ? ` ← ${ORIGIN_NAME}` : i === path.length - 1 ? ' ← Ka\'bah' : '';
console.log(` ${i + 1}. ${lat.toFixed(4)}°, ${lng.toFixed(4)}° (+${km.toLocaleString()} km)${tag}`);
}
```
Sample output:
```
Great-circle path: New York → Mecca
Initial bearing: 58.49°
Total distance: 9,139 km
Waypoints (8):
1. 40.7128°, -74.0060° (+0 km) ← New York
2. 47.2391°, -56.2891° (+1,305 km)
3. 53.1093°, -35.4823° (+2,610 km)
4. 57.6212°, -10.4521° (+3,915 km)
5. 60.0301°, 18.1842° (+5,220 km)
6. 59.7034°, 44.5781° (+6,525 km)
7. 56.2941°, 64.7329° (+7,830 km)
8. 21.4225°, 39.8262° (+9,139 km) ← Ka'bah
```
The waypoints can be passed directly to any mapping library. For Leaflet:
```js
const latLngs = path.map(([lat, lng]) => [lat, lng]);
L.polyline(latLngs, { color: 'green' }).addTo(map);
```

52
.github/wiki/examples/qibla-lookup.md vendored Normal file
View file

@ -0,0 +1,52 @@
# Example: Qibla Lookup for Multiple Cities
Print Qibla bearing and distance for a set of global cities.
```js
import { qiblaAngle, compassName, distanceKm, KAABA_LAT, KAABA_LNG } from '@acamarata/qibla';
const cities = [
{ name: 'New York', lat: 40.7128, lng: -74.0060 },
{ name: 'London', lat: 51.5074, lng: -0.1278 },
{ name: 'Istanbul', lat: 41.0082, lng: 28.9784 },
{ name: 'Nairobi', lat: -1.2921, lng: 36.8219 },
{ name: 'Karachi', lat: 24.8607, lng: 67.0011 },
{ name: 'Kuala Lumpur', lat: 3.1390, lng: 101.6869 },
{ name: 'Jakarta', lat: -6.2088, lng: 106.8456 },
{ name: 'Sydney', lat: -33.8688, lng: 151.2093 },
];
console.log('Qibla directions from major cities\n');
console.log(`${'City'.padEnd(18)} ${'Bearing'.padStart(8)} ${'Direction'.padEnd(14)} Distance`);
console.log('─'.repeat(62));
for (const city of cities) {
const bearing = qiblaAngle(city.lat, city.lng);
const dir = compassName(bearing);
const km = distanceKm(city.lat, city.lng, KAABA_LAT, KAABA_LNG);
console.log(
city.name.padEnd(18) +
` ${bearing.toFixed(1).padStart(7)}°` +
` ${dir.padEnd(14)}` +
` ${Math.round(km).toLocaleString()} km`
);
}
```
Sample output:
```
Qibla directions from major cities
City Bearing Direction Distance
──────────────────────────────────────────────────────────────
New York 58.5° Northeast 9,139 km
London 119.0° Southeast 4,950 km
Istanbul 36.6° Northeast 2,620 km
Nairobi 29.8° Northeast 3,618 km
Karachi 64.8° Northeast 1,932 km
Kuala Lumpur 292.5° West-northwest 6,354 km
Jakarta 292.5° West-northwest 7,756 km
Sydney 278.0° West 1,365 km
```

98
.github/wiki/guides/advanced.md vendored Normal file
View file

@ -0,0 +1,98 @@
# Advanced Usage
## Compass overlay integration
The eight-point compass abbreviation is suited for UI labels. Map the bearing to a rotation for a needle overlay:
```js
import { qiblaAngle } from '@acamarata/qibla';
const bearing = qiblaAngle(lat, lng);
// CSS rotation for a compass needle pointing north by default
const rotation = `rotate(${bearing}deg)`;
needle.style.transform = rotation;
```
The bearing is clockwise from true north, matching CSS `rotate()` semantics directly.
## Dense great-circle paths
Pass a higher step count for smoother polylines on a map:
```js
import { qiblaGreatCircle, KAABA_LAT, KAABA_LNG } from '@acamarata/qibla';
const observer = [40.7128, -74.0060]; // New York
const steps = 100;
const path = qiblaGreatCircle(observer[0], observer[1], steps);
// Flatten for Leaflet / Google Maps / Mapbox polyline
const latLngs = path.map(([lat, lng]) => ({ lat, lng }));
```
The default step count is 20. Steps above 200 are rarely useful for display.
## Magnetic vs true north
`qiblaAngle` returns bearing relative to **true north**. For a physical compass, apply the local magnetic declination:
```js
import { qiblaAngle } from '@acamarata/qibla';
const trueBearing = qiblaAngle(lat, lng);
// Magnetic declination for New York (west = negative)
const declination = -13.2;
const magneticBearing = (trueBearing - declination + 360) % 360;
console.log(`Magnetic: ${magneticBearing.toFixed(1)}°`);
```
Magnetic declination varies by location and year. Use a current value from NOAA's World Magnetic Model or a service like `@mapbox/geodeticsurvey`.
## Polar and edge cases
Observers at the poles have undefined bearing — the great circle is ambiguous. `qiblaAngle` returns `NaN` for latitudes exactly at ±90. Check before using:
```js
const bearing = qiblaAngle(90, 0); // NaN — north pole
if (!isFinite(bearing)) {
console.log('Bearing undefined at this location.');
}
```
Observers very close to Mecca (within ~1 km) may get erratic bearings due to floating-point precision near the target. No special handling is needed in practice.
## Batch calculation for multiple cities
```js
import { qiblaAngle, compassDir, distanceKm, KAABA_LAT, KAABA_LNG } from '@acamarata/qibla';
const cities = [
{ name: 'New York', lat: 40.7128, lng: -74.0060 },
{ name: 'London', lat: 51.5074, lng: -0.1278 },
{ name: 'Istanbul', lat: 41.0082, lng: 28.9784 },
{ name: 'Kuala Lumpur', lat: 3.1390, lng: 101.6869 },
{ name: 'Sydney', lat: -33.8688, lng: 151.2093 },
];
console.log('City'.padEnd(16) + 'Bearing Dir Distance');
console.log('-'.repeat(46));
for (const c of cities) {
const b = qiblaAngle(c.lat, c.lng);
const dir = compassDir(b);
const km = distanceKm(c.lat, c.lng, KAABA_LAT, KAABA_LNG);
console.log(
c.name.padEnd(16) +
`${b.toFixed(1).padStart(6)}° ${dir.padEnd(4)} ${Math.round(km).toLocaleString()} km`
);
}
```
## Related pages
- [API Reference](../API-Reference) — parameter types, return values, thrown errors
- [Architecture](../Architecture) — algorithm details, coordinate system, Ka'bah coordinates

69
.github/wiki/guides/quickstart.md vendored Normal file
View file

@ -0,0 +1,69 @@
# Quick Start
Five minutes from install to Qibla direction.
## Install
```sh
npm install @acamarata/qibla
```
## Basic usage
```js
import { qiblaAngle, compassDir, compassName } from '@acamarata/qibla';
const LAT = 40.7128; // New York
const LNG = -74.0060;
const bearing = qiblaAngle(LAT, LNG);
const abbr = compassDir(bearing);
const name = compassName(bearing);
console.log(`Qibla: ${bearing.toFixed(2)}° (${name}, ${abbr})`);
// Qibla: 58.49° (Northeast, NE)
```
## Distance to Mecca
```js
import { distanceKm, KAABA_LAT, KAABA_LNG } from '@acamarata/qibla';
const km = distanceKm(40.7128, -74.0060, KAABA_LAT, KAABA_LNG);
console.log(`Distance to Ka'bah: ${Math.round(km).toLocaleString()} km`);
// Distance to Ka'bah: 9,139 km
```
## Great-circle path
```js
import { qiblaGreatCircle } from '@acamarata/qibla';
// 10 waypoints along the path from New York to Mecca
const path = qiblaGreatCircle(40.7128, -74.0060, 10);
for (const [lat, lng] of path) {
console.log(` ${lat.toFixed(2)}, ${lng.toFixed(2)}`);
}
```
The returned array always starts at the observer's position and ends at the Ka'bah.
## Input validation
All functions throw `RangeError` on invalid coordinates:
```js
import { qiblaAngle } from '@acamarata/qibla';
try {
qiblaAngle(200, 0); // lat out of range
} catch (err) {
console.error(err.message); // "lat must be in range [-90, 90]"
}
```
## Next steps
- [API Reference](../API-Reference) — full function and constant documentation
- [Advanced Guide](advanced) — compass overlay, map integration, path interpolation

View file

@ -15,12 +15,12 @@ jobs:
node-version: [20, 22, 24]
steps:
- uses: actions/checkout@v4
- name: Enable corepack
run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: pnpm
- name: Enable corepack
run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: node --test test.mjs
@ -31,12 +31,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Enable corepack
run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Enable corepack
run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm run lint
- run: pnpm run format:check
@ -46,12 +46,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Enable corepack
run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Enable corepack
run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm run typecheck
@ -60,12 +60,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Enable corepack
run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Enable corepack
run: corepack enable
- run: pnpm install --frozen-lockfile
- run: pnpm build
- name: Verify pack contents
@ -79,3 +79,24 @@ jobs:
grep "CHANGELOG.md" pack-output.txt
grep "LICENSE" pack-output.txt
echo "Pack check passed"
coverage:
name: Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Enable corepack
run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: pnpm run coverage
- name: Upload to Codecov
uses: codecov/codecov-action@v4
with:
files: ./coverage/lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false

View file

@ -3,7 +3,7 @@ name: Sync Wiki
on:
push:
branches: [main]
paths: [".github/wiki/**"]
paths: ['.github/wiki/**']
workflow_dispatch:
permissions:
@ -23,7 +23,7 @@ jobs:
echo "wiki_exists=true" >> "$GITHUB_OUTPUT"
else
echo "wiki_exists=false" >> "$GITHUB_OUTPUT"
echo "Wiki not yet initialized — skipping sync. Initialize via GitHub web UI first."
echo "Wiki not yet initialized. Create the first page once via the GitHub web UI; subsequent runs will sync .github/wiki automatically."
fi
- name: Sync wiki pages

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
node_modules/
dist/
coverage/
*.tgz
*.log
.DS_Store

View file

@ -1 +0,0 @@
{}

View file

@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.1.2] - 2026-05-30
### Changed
- Add TSDoc comments with examples and wiki links to all exported functions
- Add non-null assertions with explanatory comments for array index access
- Formatting cleanup (inline multi-line function signatures)
## [1.1.1] - 2026-05-28
### Changed

View file

@ -3,6 +3,7 @@
[![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)
[![Wiki](https://img.shields.io/badge/docs-wiki-blue.svg)](https://github.com/acamarata/qibla/wiki)
Qibla direction, great-circle path, and haversine distance. Pure math, zero dependencies.
@ -55,6 +56,11 @@ Full API reference, algorithm design, and spherical trigonometry notes: [GitHub
Ka'bah coordinates verified against published GPS surveys and cross-checked with satellite imagery. Forward azimuth formula follows standard spherical trigonometry as used in aviation and geodesy.
## Telemetry
This package supports opt-in anonymous usage telemetry — off by default.
Enable: `ACAMARATA_TELEMETRY=1`. See [TELEMETRY.md](./TELEMETRY.md) for what is sent and how to disable.
## License
[MIT](LICENSE)

8
TELEMETRY.md Normal file
View file

@ -0,0 +1,8 @@
# Telemetry Disclosure
This package supports opt-in anonymous usage telemetry via [`@acamarata/telemetry`](https://github.com/acamarata/telemetry).
Telemetry is **off by default**. No data is sent unless you set `ACAMARATA_TELEMETRY=1`.
Full disclosure (what is sent, where it goes, how to disable):
[github.com/acamarata/telemetry/blob/main/TELEMETRY.md](https://github.com/acamarata/telemetry/blob/main/TELEMETRY.md)

View file

@ -1,10 +1,30 @@
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import tsParser from '@typescript-eslint/parser';
import tsPlugin from '@typescript-eslint/eslint-plugin';
import eslintConfigPrettier from 'eslint-config-prettier';
import { typescript } from '@acamarata/eslint-config';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
const __dirname = dirname(fileURLToPath(import.meta.url));
export default [
{
ignores: ["dist/", "*.cjs"],
files: ['src/**/*.ts'],
plugins: { '@typescript-eslint': tsPlugin },
languageOptions: {
parser: tsParser,
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
},
},
);
{
files: ['src/**/*.ts'],
...typescript.reduce((acc, cfg) => ({ ...acc, ...cfg }), {}),
},
eslintConfigPrettier,
{
ignores: ['dist/', 'node_modules/', '*.cjs', '*.mjs', 'tsup.config.ts', 'eslint.config.js'],
},
];

View file

@ -1,6 +1,6 @@
{
"name": "@acamarata/qibla",
"version": "1.1.1",
"version": "1.1.2",
"description": "Qibla direction, great-circle path, and haversine distance. Pure math, zero dependencies.",
"author": "Aric Camarata",
"license": "MIT",
@ -34,10 +34,12 @@
"pretest": "tsup",
"test": "node --test test.mjs && node --test test-cjs.cjs",
"lint": "eslint .",
"format": "prettier --write .",
"format:check": "prettier --check .",
"prepublishOnly": "tsup",
"coverage": "c8 --reporter=lcov --reporter=text node --test"
"format": "prettier --write src/",
"format:check": "prettier --check src/",
"prepack": "pnpm run build",
"coverage": "c8 --reporter=lcov --reporter=text node test.mjs",
"docs": "typedoc --out .github/wiki/api src/index.ts",
"postbuild": "cp dist/index.d.ts dist/index.d.mts"
},
"repository": {
"type": "git",
@ -68,14 +70,25 @@
"registry": "https://registry.npmjs.org/"
},
"devDependencies": {
"@acamarata/eslint-config": "^0.1.0",
"@acamarata/prettier-config": "^0.1.0",
"@acamarata/tsconfig": "^0.1.0",
"@eslint/js": "^9.27.0",
"@types/node": "^22.15.3",
"@typescript-eslint/eslint-plugin": "^8.56.1",
"@typescript-eslint/parser": "^8.56.1",
"c8": "^11.0.0",
"eslint": "^9.27.0",
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.5.3",
"tsup": "^8.4.0",
"typedoc": "^0.28.19",
"typedoc-plugin-markdown": "^4.11.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.1"
"typescript-eslint": "^8.32.1",
"@acamarata/telemetry": "^0.1.0"
},
"type": "module",
"packageManager": "pnpm@10.11.1"
"packageManager": "pnpm@10.11.1",
"prettier": "@acamarata/prettier-config"
}

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,8 @@
*
* Ka'bah coordinates sourced from verified GPS data.
*
* SPORT: packages.md @acamarata/qibla row
*
* @module
*/
@ -34,6 +36,12 @@ const DEG = Math.PI / 180;
* @param lng - Observer longitude in decimal degrees (-180 to 180).
* @returns Bearing in degrees clockwise from north (0 = N, 90 = E, 180 = S, 270 = W).
* @throws {RangeError} If latitude is outside [-90, 90] or longitude outside [-180, 180].
*
* @example
* qiblaAngle(40.7128, -74.006); // ~58.49 (New York)
* qiblaAngle(51.5074, -0.1278); // ~119.0 (London)
*
* @see {@link https://github.com/acamarata/qibla/wiki/api/qiblaAngle} Wiki API page
*/
export function qiblaAngle(lat: number, lng: number): number {
if (lat < -90 || lat > 90) {
@ -47,14 +55,10 @@ export function qiblaAngle(lat: number, lng: number): number {
const φ2 = KAABA_LAT * DEG,
λ2 = KAABA_LNG * DEG;
const y = Math.sin(λ2 - λ1) * Math.cos(φ2);
const x =
Math.cos(φ1) * Math.sin(φ2) -
Math.sin(φ1) * Math.cos(φ2) * Math.cos(λ2 - λ1);
const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(λ2 - λ1);
return (Math.atan2(y, x) / DEG + 360) % 360;
}
/**
* Eight-point compass abbreviation for a bearing.
*
@ -62,7 +66,9 @@ export function qiblaAngle(lat: number, lng: number): number {
* @returns Two-letter compass abbreviation (N, NE, E, SE, S, SW, W, NW).
*/
export function compassDir(bearing: number): CompassAbbr {
return COMPASS_ABBR[Math.round(bearing / 45) % 8];
// Non-null assertion: index is always 0-7 (Math.round(bearing/45) % 8), which is within COMPASS_ABBR bounds.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return COMPASS_ABBR[Math.round(bearing / 45) % 8]!;
}
/**
@ -72,7 +78,9 @@ export function compassDir(bearing: number): CompassAbbr {
* @returns Full direction name (North, Northeast, etc.).
*/
export function compassName(bearing: number): CompassName {
return COMPASS_NAMES[Math.round(bearing / 45) % 8];
// Non-null assertion: index is always 0-7 (Math.round(bearing/45) % 8), which is within COMPASS_NAMES bounds.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return COMPASS_NAMES[Math.round(bearing / 45) % 8]!;
}
/**
@ -87,11 +95,7 @@ export function compassName(bearing: number): CompassName {
* @returns Array of [latitude, longitude] pairs in degrees.
* @throws {RangeError} If latitude is outside [-90, 90] or longitude outside [-180, 180].
*/
export function qiblaGreatCircle(
lat: number,
lng: number,
steps = 120,
): [number, number][] {
export function qiblaGreatCircle(lat: number, lng: number, steps = 120): [number, number][] {
if (lat < -90 || lat > 90) {
throw new RangeError(`Latitude must be between -90 and 90, got ${lat}`);
}
@ -107,8 +111,7 @@ export function qiblaGreatCircle(
2 *
Math.asin(
Math.sqrt(
Math.sin((φ2 - φ1) / 2) ** 2 +
Math.cos(φ1) * Math.cos(φ2) * Math.sin((λ2 - λ1) / 2) ** 2,
Math.sin((φ2 - φ1) / 2) ** 2 + Math.cos(φ1) * Math.cos(φ2) * Math.sin((λ2 - λ1) / 2) ** 2,
),
);
@ -122,10 +125,7 @@ export function qiblaGreatCircle(
const x = A * Math.cos(φ1) * Math.cos(λ1) + B * Math.cos(φ2) * Math.cos(λ2);
const y = A * Math.cos(φ1) * Math.sin(λ1) + B * Math.cos(φ2) * Math.sin(λ2);
const z = A * Math.sin(φ1) + B * Math.sin(φ2);
points.push([
Math.atan2(z, Math.sqrt(x * x + y * y)) / DEG,
Math.atan2(y, x) / DEG,
]);
points.push([Math.atan2(z, Math.sqrt(x * x + y * y)) / DEG, Math.atan2(y, x) / DEG]);
}
return points;
}
@ -139,16 +139,19 @@ export function qiblaGreatCircle(
* @param lng2 - Second point longitude in decimal degrees.
* @returns Distance in kilometers (spherical Earth approximation).
*/
export function distanceKm(
lat1: number,
lng1: number,
lat2: number,
lng2: number,
): number {
export function distanceKm(lat1: number, lng1: number, lat2: number, lng2: number): number {
const dLat = (lat2 - lat1) * DEG;
const dLng = (lng2 - lng1) * DEG;
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos(lat1 * DEG) * Math.cos(lat2 * DEG) * Math.sin(dLng / 2) ** 2;
Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * DEG) * Math.cos(lat2 * DEG) * Math.sin(dLng / 2) ** 2;
return EARTH_RADIUS_KM * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
// ── Opt-in anonymous telemetry ────────────────────────────────────────────────
// Off by default. Enable: ACAMARATA_TELEMETRY=1
// What is sent + how to disable: https://github.com/acamarata/telemetry/blob/main/TELEMETRY.md
import("@acamarata/telemetry")
.then(({ track }) => track("load", { package: "@acamarata/qibla", version: "1.1.2" }))
.catch(() => {
// telemetry not installed or disabled — that is fine
});

View file

@ -27,4 +27,3 @@ export type CompassAbbr = (typeof COMPASS_ABBR)[number];
/** Compass full name type. */
export type CompassName = (typeof COMPASS_NAMES)[number];

View file

@ -1,16 +1,9 @@
{
"extends": "@acamarata/tsconfig/tsconfig.library.json",
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"types": ["node"]
},

10
typedoc.json Normal file
View file

@ -0,0 +1,10 @@
{
"entryPoints": ["src/index.ts"],
"out": ".github/wiki/api",
"plugin": ["typedoc-plugin-markdown"],
"readme": "none",
"skipErrorChecking": false,
"excludePrivate": true,
"excludeProtected": true,
"includeVersion": true
}