Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 27 additions & 37 deletions DEVELOPING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,24 @@ This guide covers working with the TypeScript codebase for arc.js.

```bash
npm install # Install dependencies
npm run build # Build all outputs
npm run build # Build ESM output
npm test # Run TypeScript tests
npm run test:all # Run all tests (TypeScript + build validation)
```

## Project Structure

```
```text
src/
├── index.ts # Main entry point
├── coord.ts # Coordinate class
├── arc.ts # Arc class
├── arc.ts # Arc class
├── great-circle.ts # Great circle calculations
├── line-string.ts # Internal geometry helper
├── utils.ts # Utility functions
└── types.ts # TypeScript type definitions

test/
├── *.test.ts # Jest TypeScript tests (source code)
└── build-output.test.js # Build validation (compiled output)
└── *.test.ts # Jest TypeScript tests
```

## Development Workflow
Expand All @@ -36,14 +34,11 @@ test/
# Run TypeScript tests (fast, for development)
npm test

# Run build validation (slower, tests compiled output)
npm run test:build

# Run everything (recommended before committing)
npm run test:all

# Watch mode for development
npm run test:watch

# Coverage report
npm run test:coverage
```

### Building
Expand All @@ -52,10 +47,7 @@ npm run test:watch
npm run build
```

This generates:
- `dist/` - CommonJS output with `.d.ts` files
- `dist/esm/` - ES modules output
- `arc.js` - Browser bundle (UMD format)
This generates `dist/` — ESM output with `.d.ts` declaration files.

## Publishing

Expand All @@ -68,7 +60,7 @@ This generates:

### Pre-publish Checklist (for maintainers)

1. **Tests pass**: `npm run test:all`
1. **Tests pass**: `npm test`
2. **Build succeeds**: `npm run build`
3. **Version updated**: Update `package.json` version
4. **Changelog updated**: Document changes
Expand All @@ -77,49 +69,47 @@ This generates:
### Publishing Process (maintainers only)

```bash
npm run build # Builds automatically on prepublishOnly
npm publish
npm publish # prepublishOnly runs npm run build automatically
```

The `prepublishOnly` script ensures a fresh build before publishing.

### What Gets Published

- `dist/` folder (compiled JS + TypeScript definitions)
- `arc.js` browser bundle
- `dist/` folder (compiled ESM JS + TypeScript definitions)
- `README.md`, `LICENSE.md`, `CHANGELOG.md`

## TypeScript Development

### TypeScript Configuration

- **Source**: Modern TypeScript with strict settings
- **Output**: ES2022 for broad compatibility
- **Paths**: `@/` alias maps to `src/` in tests
- **Output**: ES2022, ESM only
- **Declarations**: Full `.d.ts` generation for consumers

### Adding New Types

1. Add interfaces/types to `src/types.ts`. You can see that it makes use of some GeoJSON types, but in the future it may want to use more of them.
1. Add interfaces/types to `src/types.ts`
2. Export public types from `src/index.ts`
3. Import types with `import type { ... }`
4. Add tests in relevant `test/*.test.ts` files including typescript.test.ts

## Usage & Module Formats
4. Add tests in relevant `test/*.test.ts` files including `typescript.test.ts`

The package supports multiple import styles:
## Usage

```javascript
// CommonJS (Node.js)
const { GreatCircle } = require('arc');

// ES Modules
// ES Modules (Node.js or bundler)
import { GreatCircle } from 'arc';
```

// Browser (UMD bundle)
<script src="arc.js"></script>
## Visual Fixture Verification

To inspect all test routes as great circle arcs on a map:

```bash
npm run build # dist/ must exist
node scripts/dump-fixtures.mjs | pbcopy # macOS: copy to clipboard
```

All formats are tested in `test/build-output.test.js`.
Then, paste the geojson output into a visualization tool to visually verify routes, such as [geojson.io](https://geojson.io).
**Note:** route coordinates in the script are manually updated to keep in sync with `test/fixtures/routes.ts`.

## Common Tasks

Expand Down
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,11 @@ const gc = new GreatCircle(start, end, { name: 'Seattle to DC' });

#### 3. Generate the arc
```js
const line = gc.Arc(100, { offset: 10 });
const line = gc.Arc(100);
```

**Parameters:**
- `npoints` (number): Number of intermediate points (higher = more accurate)
- `options.offset` (number): Dateline crossing threshold in degrees (default: 10)

### TypeScript Support

Expand All @@ -87,8 +86,7 @@ const end: CoordinatePoint = { x: -77, y: 39 };
const properties: RouteProperties = { name: 'Seattle to DC', color: 'blue' };

const gc = new GreatCircle(start, end, properties);
const options: ArcOptions = { offset: 10 };
const line = gc.Arc(100, options);
const line = gc.Arc(100);

// Fully typed return values
const geojson = line.json(); // GeoJSONFeature
Expand Down Expand Up @@ -144,7 +142,7 @@ const wkt = line.wkt();

### Dateline Crossing

The library automatically handles routes that cross the international dateline. The `offset` option (default: 10) controls how close to the dateline a route must be before it gets split into multiple segments. For routes near the poles, you may need a higher offset value.
Routes that cross the international dateline are automatically detected and split into a `MultiLineString` with exact `±180°` boundary points. No configuration is needed.

## Examples

Expand Down
14 changes: 2 additions & 12 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -333,11 +333,6 @@ <h3>Settings</h3>
<input type="number" id="npoints" value="100" min="10" max="1000" />
</div>

<div class="control-group">
<label for="offset">Dateline offset (degrees):</label>
<input type="number" id="offset" value="20" min="1" max="90" step="1" />
</div>

<div class="control-group">
<button id="clear" class="btn">Clear All</button>
<button id="reset" class="btn btn-danger">Reset View</button>
Expand Down Expand Up @@ -377,7 +372,6 @@ <h4>Generated GeoJSON</h4>

// Configuration
var npoints = 100;
var offset = 20;
var coords = [];
var points = [];
var snap_tolerance = 500000;
Expand All @@ -390,10 +384,6 @@ <h4>Generated GeoJSON</h4>
npoints = parseInt(this.value) || 100;
});

document.getElementById('offset').addEventListener('change', function() {
offset = parseInt(this.value) || 20;
});

var start, end;

function draw(coords) {
Expand Down Expand Up @@ -469,7 +459,7 @@ <h4>Generated GeoJSON</h4>
};

var greatCircle = new GreatCircle(from, to, properties);
var gc = greatCircle.Arc(npoints, { offset: offset });
var gc = greatCircle.Arc(npoints);
var line = new L.geoJson().addTo(map);
var geojson_feature = gc.json();

Expand Down Expand Up @@ -592,7 +582,7 @@ <h4>Generated GeoJSON</h4>

try {
var greatCircle = new GreatCircle(nyc, london, properties);
var gc = greatCircle.Arc(npoints, { offset: offset });
var gc = greatCircle.Arc(npoints);
var line = new L.geoJson().addTo(map);
var geojson_feature = gc.json();

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
],
"contributors": [
"Dane Springmeyer <dane.springmeyer@gmail.com>",
"John Gravois <jagravois@gmail.com>"
"John Gravois <jagravois@gmail.com>",
"Thomas Hervey <thomasahervey@gmail.com>"
],
"repository": {
"type": "git",
Expand Down
134 changes: 134 additions & 0 deletions scripts/benchmark.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* Benchmarks antimeridian bisection (current) vs linear interpolation (old GDAL heuristic).
*
* The old approach linearly interpolated the crossing latitude from the two already-computed
* adjacent sample points — zero additional interpolate() calls.
*
* The new approach runs 50 bisection iterations (2 interpolate() calls each = 100 calls)
* per antimeridian crossing to find the exact latitude.
*
* Usage:
* node scripts/benchmark.mjs
*
* Requires a built dist/: run `npm run build` first.
*/

import { GreatCircle } from '../dist/index.js';

// ---------------------------------------------------------------------------
// Routes: one non-crossing (control) and three antimeridian crossings.
// All taken from test/fixtures/routes.ts.
// ---------------------------------------------------------------------------

const ROUTES = {
'Seattle → DC (non-crossing)': { start: { x: -122, y: 48 }, end: { x: -77, y: 39 } },
'Tokyo → LAX (1 crossing)': { start: { x: 139.7798, y: 35.5494 }, end: { x: -118.4085, y: 33.9416 } },
'Auckland → LAX (1 crossing)': { start: { x: 174.79, y: -36.85 }, end: { x: -118.41, y: 33.94 } },
'Shanghai → SFO (1 crossing)': { start: { x: 121.81, y: 31.14 }, end: { x: -122.38, y: 37.62 } },
};

const NPOINTS_VALUES = [10, 100, 1000];
const REPS = 2000; // repetitions per (route × npoints) cell

// ---------------------------------------------------------------------------
// Baseline: linear interpolation (mirrors the old GDAL heuristic approach).
// When |Δlon| > 180, linearly interpolate the crossing latitude from the two
// adjacent already-computed sample points — no additional interpolate() calls.
// ---------------------------------------------------------------------------

function arcLinear(gc, npoints) {
if (!npoints || npoints <= 2) return;

const delta = 1.0 / (npoints - 1);
const points = [];
for (let i = 0; i < npoints; i++) {
points.push(gc.interpolate(delta * i));
}

const segments = [];
let current = [];

for (let i = 0; i < points.length; i++) {
const pt = points[i];
if (i === 0) { current.push(pt); continue; }

const prev = points[i - 1];
if (Math.abs(pt[0] - prev[0]) > 180) {
// Linear interpolation: estimate crossing lat from adjacent sampled points.
// t is how far along [prev→pt] the ±180 boundary lies, using lon values.
const t = (prev[0] > 0 ? 180 - prev[0] : -180 - prev[0]) / (pt[0] - prev[0]);
const crossingLat = prev[1] + t * (pt[1] - prev[1]);
const fromEast = prev[0] > 0;
current.push([fromEast ? 180 : -180, crossingLat]);
segments.push(current);
current = [[fromEast ? -180 : 180, crossingLat]];
}

current.push(pt);
}
if (current.length > 0) segments.push(current);
return segments;
}

// ---------------------------------------------------------------------------
// Benchmark runner
// ---------------------------------------------------------------------------

function bench(label, fn, reps) {
// Warm up V8 JIT
for (let i = 0; i < 50; i++) fn();

const t0 = performance.now();
for (let i = 0; i < reps; i++) fn();
const elapsed = performance.now() - t0;

return { label, reps, totalMs: elapsed, usPerArc: (elapsed / reps) * 1000 };
}

// ---------------------------------------------------------------------------
// Run
// ---------------------------------------------------------------------------

console.log(`Benchmark: bisection vs linear interpolation`);
console.log(`${REPS} reps per cell\n`);

const header = ['Route', 'npoints', 'Method', 'µs/arc', 'overhead'];
console.log(header.join('\t'));
console.log(header.map(h => '-'.repeat(h.length)).join('\t'));

for (const [routeName, { start, end }] of Object.entries(ROUTES)) {
const gc = new GreatCircle(start, end);

for (const npoints of NPOINTS_VALUES) {
const bisection = bench(
`bisection n=${npoints}`,
() => gc.Arc(npoints),
REPS
);

const linear = bench(
`linear-interp n=${npoints}`,
() => arcLinear(gc, npoints),
REPS
);

const overhead = ((bisection.usPerArc - linear.usPerArc) / linear.usPerArc * 100).toFixed(1);
const overheadStr = overhead > 0 ? `+${overhead}%` : `${overhead}%`;

console.log([
routeName,
npoints,
'bisection',
bisection.usPerArc.toFixed(2),
overheadStr,
].join('\t'));
console.log([
'',
'',
'linear (baseline)',
linear.usPerArc.toFixed(2),
'',
].join('\t'));
}
console.log();
}
Loading