@zukall/zap 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PROTOCOL.md +1 -1
- package/README.md +243 -14
- package/asset/zukall-haiti-country-grid.webp +0 -0
- package/dist/cjs/distance.d.ts +8 -0
- package/dist/cjs/distance.js +61 -0
- package/dist/cjs/index.d.ts +11 -2
- package/dist/cjs/index.js +9 -1
- package/dist/cjs/locations.d.ts +11 -0
- package/dist/cjs/locations.js +67 -0
- package/dist/cjs/route.d.ts +10 -0
- package/dist/cjs/route.js +289 -0
- package/dist/cjs/types.d.ts +70 -0
- package/dist/esm/distance.d.ts +8 -0
- package/dist/esm/distance.js +55 -0
- package/dist/esm/index.d.ts +11 -2
- package/dist/esm/index.js +6 -1
- package/dist/esm/locations.d.ts +11 -0
- package/dist/esm/locations.js +61 -0
- package/dist/esm/route.d.ts +10 -0
- package/dist/esm/route.js +285 -0
- package/dist/esm/types.d.ts +70 -0
- package/package.json +2 -2
package/PROTOCOL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# ZAP — Zukall Addressing Protocol, version 2
|
|
2
2
|
|
|
3
3
|
Protocol 2 uses **100-meter grid blocks** and **1-meter detail**, computed from
|
|
4
|
-
unrounded projected GPS coordinates. The npm package version (`0.
|
|
4
|
+
unrounded projected GPS coordinates. The npm package version (`0.3.0`) and
|
|
5
5
|
protocol version (`2`) are separate. Previously issued protocol-1 addresses retain
|
|
6
6
|
their original text and identity; see [PROTOCOL-V1.md](PROTOCOL-V1.md).
|
|
7
7
|
|
package/README.md
CHANGED
|
@@ -1,19 +1,51 @@
|
|
|
1
1
|
# ZAP — Zukall Addressing Protocol
|
|
2
2
|
|
|
3
3
|
**`@zukall/zap`** generates addresses directly from latitude and longitude using
|
|
4
|
-
fixed country/province grids.
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
fixed country/province grids.
|
|
5
|
+
|
|
6
|
+
**Give real places a recognizable address, without waiting for street names or
|
|
7
|
+
house numbers.**
|
|
8
|
+
|
|
9
|
+
Imagine building a courier application that serves Uganda, Haiti, Florida in
|
|
10
|
+
the United States, and Germany. Your couriers and remote dispatchers need to
|
|
11
|
+
recognize addresses quickly, even when those places use different languages,
|
|
12
|
+
street names, and numbering systems.
|
|
13
|
+
|
|
14
|
+
With ZAP, they learn one address format and use it across the configured
|
|
15
|
+
countries and states. A home, business, rural entrance, or pickup point can have
|
|
16
|
+
an address based on its GPS location. A dispatcher can read the same address
|
|
17
|
+
structure in each market. Your application can show a courier where a
|
|
18
|
+
destination lies relative to their current location in the same grid, then open
|
|
19
|
+
map directions using the destination’s saved GPS point.
|
|
20
|
+
|
|
21
|
+
Once you have the GPS coordinates, you can generate the address offline,
|
|
22
|
+
without a geocoding service, API key, or per-address fee. ZAP works alongside
|
|
23
|
+
existing street addresses and also gives you a location address where street
|
|
24
|
+
names or house numbers are missing.
|
|
7
25
|
|
|
8
26
|
## See how ZAP works: Haiti’s grid
|
|
9
27
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
28
|
+
ZAP starts with a configured rectangle around a country. Where a country uses
|
|
29
|
+
province or state grids, it uses the selected province’s rectangle instead—for
|
|
30
|
+
example, Florida’s grid within the United States.
|
|
31
|
+
|
|
32
|
+
Each rectangle has a fixed center and two axes: north/south and east/west.
|
|
33
|
+
Think of laying a grid of **100 × 100-meter square blocks** across the area.
|
|
34
|
+
The main address numbers count complete blocks from the center, and the
|
|
35
|
+
direction letters tell you which side of the center the location is on.
|
|
36
|
+
|
|
37
|
+
A suffix such as **`#23-43`** adds the position within that block: 23 whole meters
|
|
38
|
+
along the address’s north/south direction and 43 along its east/west direction.
|
|
39
|
+
That gives a home, entrance, or pickup point a location code with whole-meter
|
|
40
|
+
detail. Keep apartment and unit numbers alongside the code as separate details.
|
|
41
|
+
|
|
42
|
+
The Haiti illustrations below show the same fixed grid in use. Each address
|
|
43
|
+
comes from its own GPS point. Roads can run straight, diagonally, or curve;
|
|
44
|
+
the grid’s center and axes stay fixed.
|
|
13
45
|
|
|
14
46
|
### Whole-country grid and fixed center
|
|
15
47
|
|
|
16
|
-

|
|
17
49
|
|
|
18
50
|
The Haiti grid has one fixed center: **19.0578675° N, 73.0641385° W**.
|
|
19
51
|
It is the center of the configured rectangle, in the Gulf of Gonâve, rather than
|
|
@@ -44,7 +76,7 @@ The examples and captions below use the current dashed format.
|
|
|
44
76
|
|
|
45
77
|
### North–south: easting stays fixed
|
|
46
78
|
|
|
47
|
-

|
|
48
80
|
|
|
49
81
|
```text
|
|
50
82
|
Point A: 580 S 772 E #42-68, HT ZAP
|
|
@@ -58,7 +90,7 @@ Moving north on this side of the origin decreases the south block.
|
|
|
58
90
|
|
|
59
91
|
### East–west: northing stays fixed
|
|
60
92
|
|
|
61
|
-

|
|
62
94
|
|
|
63
95
|
```text
|
|
64
96
|
Point A: 582 S 770 E #42-68, HT ZAP
|
|
@@ -72,7 +104,7 @@ Moving west on this side of the origin decreases the east block.
|
|
|
72
104
|
|
|
73
105
|
### Diagonal streets: both axes can change
|
|
74
106
|
|
|
75
|
-

|
|
76
108
|
|
|
77
109
|
```text
|
|
78
110
|
Point A: 580 S 770 E #43-46, HT ZAP
|
|
@@ -191,7 +223,7 @@ npm install @zukall/zap
|
|
|
191
223
|
Before publication, install the verified local package:
|
|
192
224
|
|
|
193
225
|
```sh
|
|
194
|
-
npm install /absolute/path/to/zukall-address-protocol/.artifacts/zukall-zap-0.
|
|
226
|
+
npm install /absolute/path/to/zukall-address-protocol/.artifacts/zukall-zap-0.3.0.tgz
|
|
195
227
|
```
|
|
196
228
|
|
|
197
229
|
Build and verify the source:
|
|
@@ -449,6 +481,196 @@ API key. You can also pass `saved.coordinates` to your own map provider. Never
|
|
|
449
481
|
use the formatted ZAP string, grid center, city name, or coordinate suffix as the
|
|
450
482
|
routing destination.
|
|
451
483
|
|
|
484
|
+
## Measure distance between locations
|
|
485
|
+
|
|
486
|
+
**ZAP’s strongest advantage here is making distance understandable to people;
|
|
487
|
+
software can handle either easily.** Within one grid, a difference of three
|
|
488
|
+
blocks along an axis represents 300 projected meters. That is easier for a
|
|
489
|
+
person to read than comparing decimal GPS coordinates. Software can calculate
|
|
490
|
+
distance from either representation.
|
|
491
|
+
|
|
492
|
+
Use complete saved records whenever available. `getDistance` defaults to the
|
|
493
|
+
original GPS coordinates, including when the records come from different
|
|
494
|
+
countries, states, or protocol versions:
|
|
495
|
+
|
|
496
|
+
```js
|
|
497
|
+
import { generateAddress, getDistance } from "@zukall/zap";
|
|
498
|
+
|
|
499
|
+
const depot = generateAddress({
|
|
500
|
+
countryCode: "UG", city: "Kampala", latitude: 0.3476, longitude: 32.5825,
|
|
501
|
+
});
|
|
502
|
+
const pickup = generateAddress({
|
|
503
|
+
countryCode: "UG", city: "Kampala", latitude: 0.3486, longitude: 32.5825,
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const distance = getDistance(depot, pickup);
|
|
507
|
+
console.log(distance.method); // haversine
|
|
508
|
+
console.log(distance.meters.toFixed(1)); // 111.2
|
|
509
|
+
console.log(distance.kilometers.toFixed(4)); // 0.1112
|
|
510
|
+
|
|
511
|
+
// Latitude/longitude objects also work.
|
|
512
|
+
getDistance(depot.coordinates, pickup.coordinates);
|
|
513
|
+
|
|
514
|
+
// Optional distance in the saved grid's projected plane.
|
|
515
|
+
const gridDistance = getDistance(depot, pickup, { method: "projected" });
|
|
516
|
+
console.log(gridDistance.method); // projected
|
|
517
|
+
console.log(gridDistance.gridId === depot.gridId); // true
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
Haversine measures the geographic great-circle distance on a sphere, using the
|
|
521
|
+
same **6,371,008.8-meter Earth radius** as the address projection. Values retain
|
|
522
|
+
number precision; rounding above is only for display. It does not measure road
|
|
523
|
+
length, altitude, or terrain.
|
|
524
|
+
|
|
525
|
+
Projected mode uses `Math.hypot()` on the stored northing/easting differences.
|
|
526
|
+
Both records must have **exactly the same `gridId`** and finite offsets. A
|
|
527
|
+
different country, province, snapshot, or encoding ID is rejected. This mode
|
|
528
|
+
does not regenerate addresses or reproject older records; protocol-1 offsets
|
|
529
|
+
can retain their original snapping. Projected-plane distances can differ from
|
|
530
|
+
geographic distances away from the grid origin. Neither mode parses the address
|
|
531
|
+
text, subtracts unit numbers, or treats the suffix as a house number.
|
|
532
|
+
|
|
533
|
+
## Optimize randomly entered stops
|
|
534
|
+
|
|
535
|
+
`optimizeRoute` returns an **optimized straight-line itinerary**. It chooses an
|
|
536
|
+
order that reduces the sum of distances between consecutive stops. Your
|
|
537
|
+
application can use that order to plan visits, then open map directions from
|
|
538
|
+
each stop's saved GPS point. It does not provide driving directions or guarantee
|
|
539
|
+
the shortest journey along roads.
|
|
540
|
+
|
|
541
|
+
This example uses illustrative GPS points along a meridian, entered out of order:
|
|
542
|
+
|
|
543
|
+
```js
|
|
544
|
+
import { generateAddress, optimizeRoute } from "@zukall/zap";
|
|
545
|
+
|
|
546
|
+
const stop = (id, latitude) => ({
|
|
547
|
+
id,
|
|
548
|
+
...generateAddress({
|
|
549
|
+
countryCode: "UG", city: "Kampala", latitude, longitude: 32.5825,
|
|
550
|
+
}),
|
|
551
|
+
});
|
|
552
|
+
const stops = [
|
|
553
|
+
stop("depot", 0.3476),
|
|
554
|
+
stop("C", 0.3506),
|
|
555
|
+
stop("A", 0.3486),
|
|
556
|
+
stop("B", 0.3496),
|
|
557
|
+
];
|
|
558
|
+
|
|
559
|
+
const itinerary = optimizeRoute(stops);
|
|
560
|
+
console.log(itinerary.description); // optimized straight-line itinerary
|
|
561
|
+
console.log(itinerary.orderedStops.map(stop => stop.id)); // ["depot", "A", "B", "C"]
|
|
562
|
+
console.log(itinerary.originalIndices); // [0, 2, 3, 1]
|
|
563
|
+
console.log(itinerary.totalMeters.toFixed(1)); // 333.6
|
|
564
|
+
console.log(itinerary.originalTotalMeters.toFixed(1)); // 667.2
|
|
565
|
+
console.log(itinerary.optimization); // exact
|
|
566
|
+
console.log(itinerary.algorithm); // held-karp
|
|
567
|
+
console.log(itinerary.method); // haversine
|
|
568
|
+
|
|
569
|
+
for (const leg of itinerary.legs) {
|
|
570
|
+
console.log(stops[leg.fromIndex].id, stops[leg.toIndex].id, leg.meters);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Select endpoints by their ORIGINAL input indices.
|
|
574
|
+
const fixedEndpoints = optimizeRoute(stops, { startIndex: 2, endIndex: 1 });
|
|
575
|
+
// Starts at A (input 2), finishes at C (input 1), and includes every other stop.
|
|
576
|
+
|
|
577
|
+
const roundTrip = optimizeRoute(stops, { returnToStart: true });
|
|
578
|
+
console.log(roundTrip.legs.at(-1).isReturn); // true
|
|
579
|
+
// orderedStops still includes the depot only once; legs include the return to it.
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
| Option | Behavior |
|
|
583
|
+
| --- | --- |
|
|
584
|
+
| `startIndex` | Fixed start selected from the supplied stops; defaults to input index `0`. |
|
|
585
|
+
| `endIndex` | Optional fixed final stop, selected by original index. |
|
|
586
|
+
| `returnToStart` | Adds a closing leg to the start; defaults to `false` and cannot be combined with `endIndex`. |
|
|
587
|
+
| `method` | `"haversine"` by default, or `"projected"` when every saved record has the identical grid ID. |
|
|
588
|
+
| `resolveAddress` | Caller-owned lookup for address strings; supplying it makes the call return a Promise. |
|
|
589
|
+
| `distanceMatrix` | Optional meter costs indexed by original stop indices; cannot be combined with `method`. |
|
|
590
|
+
|
|
591
|
+
Endpoints must be supplied stops. If a depot is outside your delivery list,
|
|
592
|
+
include it in `points` and select its index. An open itinerary with multiple
|
|
593
|
+
stops cannot have the same start and end; choose `returnToStart` for that case.
|
|
594
|
+
|
|
595
|
+
The result contains the original objects or strings in `orderedStops`, their
|
|
596
|
+
`originalIndices`, `resolvedPoints` in the same order, and `legs` with
|
|
597
|
+
`fromIndex`, `toIndex`, meters, kilometers, method, and `isReturn`. Totals are
|
|
598
|
+
available as `totalMeters` and `totalKilometers`. Inputs and saved addresses are
|
|
599
|
+
not modified. Duplicate coordinates and duplicate references remain separate
|
|
600
|
+
stops and retain their own indices.
|
|
601
|
+
|
|
602
|
+
### Exact and approximate optimization
|
|
603
|
+
|
|
604
|
+
- **Up to 12 stops:** Held–Karp subset dynamic programming finds an exact minimum
|
|
605
|
+
for the selected distance costs and endpoint constraints.
|
|
606
|
+
- **13–1000 stops:** Deterministic nearest neighbor followed by 2-opt returns an
|
|
607
|
+
approximate order. Both the nearest-neighbor order and the original feasible
|
|
608
|
+
order are improved, with at most 100 2-opt passes per seed. Fixed endpoints
|
|
609
|
+
remain fixed. Results are never longer than the original feasible order.
|
|
610
|
+
|
|
611
|
+
`optimization` reports `"exact"` or `"approximate"`; `algorithm` names the
|
|
612
|
+
algorithm, and `exactCutoff` is `12`. Equal-cost choices use original input
|
|
613
|
+
indices deterministically. `ROUTE_OPTIMIZATION` exports the frozen work limits.
|
|
614
|
+
The original feasible order moves the selected start first and end last, keeping
|
|
615
|
+
the other stops in their entered order; its cost is `originalTotalMeters`.
|
|
616
|
+
|
|
617
|
+
Empty lists and single stops return an exact zero-distance itinerary with no
|
|
618
|
+
legs, even for a round trip. Explicit endpoint indices must still be valid.
|
|
619
|
+
Invalid coordinates, sparse arrays, contradictory endpoints, and lists above
|
|
620
|
+
1000 stops are rejected. Optimizing many stops uses a quadratic distance matrix;
|
|
621
|
+
approximate results are not a guarantee of the best possible order.
|
|
622
|
+
|
|
623
|
+
### Address strings need saved-record lookups
|
|
624
|
+
|
|
625
|
+
The package has **no reverse decoder or automatic geocoder**. A text-only ZAP
|
|
626
|
+
address cannot restore the original GPS point. Supply a resolver that retrieves
|
|
627
|
+
the corresponding saved record; it may return the record directly or use an
|
|
628
|
+
asynchronous database lookup:
|
|
629
|
+
|
|
630
|
+
```js
|
|
631
|
+
const savedByAddress = new Map(stops.map(record => [record.address, record]));
|
|
632
|
+
const resolveAddress = async (address, originalIndex) => {
|
|
633
|
+
const record = savedByAddress.get(address);
|
|
634
|
+
if (!record) throw new Error(`No saved GPS record for input ${originalIndex}`);
|
|
635
|
+
return record;
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
const textStops = stops.map(record => record.address);
|
|
639
|
+
const itineraryFromText = await optimizeRoute(textStops, { resolveAddress });
|
|
640
|
+
const distanceFromText = await getDistance(textStops[0], textStops[1], { resolveAddress });
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
Resolvers receive each string and its original input index (0/1 for
|
|
644
|
+
`getDistance`), once per string occurrence. The original strings remain in
|
|
645
|
+
`orderedStops`; retrieved records are in `resolvedPoints`. A resolver must return
|
|
646
|
+
a saved record with valid `coordinates`; missing records and lookup failures
|
|
647
|
+
reject the Promise. Direct GPS/record calls without a resolver stay synchronous.
|
|
648
|
+
Use full records and your own stop IDs when addresses are shared by different
|
|
649
|
+
units or records; an address string alone does not identify a unique household.
|
|
650
|
+
|
|
651
|
+
### Supplying distance costs from another provider
|
|
652
|
+
|
|
653
|
+
The optimizer consumes a distance matrix internally. Your application can supply
|
|
654
|
+
one from a future road-distance provider without changing stop identities:
|
|
655
|
+
|
|
656
|
+
```js
|
|
657
|
+
const matrix = [
|
|
658
|
+
[0, 400, 100],
|
|
659
|
+
[350, 0, 200],
|
|
660
|
+
[120, 220, 0],
|
|
661
|
+
];
|
|
662
|
+
const suppliedCosts = optimizeRoute(stops.slice(0, 3), { distanceMatrix: matrix });
|
|
663
|
+
console.log(suppliedCosts.method); // distance-matrix
|
|
664
|
+
console.log(suppliedCosts.description); // optimized distance-matrix itinerary
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
Rows/columns follow **original input order**, entries are nonnegative finite
|
|
668
|
+
**meters**, and the diagonal must be zero. Matrices may be asymmetric; both
|
|
669
|
+
algorithms account for the full cost of reversing directed legs. Values must
|
|
670
|
+
leave room for finite route totals. The package does not fetch provider data.
|
|
671
|
+
Results are labeled separately when caller-supplied costs are used. The function
|
|
672
|
+
still returns a stop order, not driving instructions or a vehicle scheduling plan.
|
|
673
|
+
|
|
452
674
|
## Upgrade from protocol 1 without changing issued addresses
|
|
453
675
|
|
|
454
676
|
Package `0.2.0` introduces protocol 2. `generateAddress` no longer accepts
|
|
@@ -544,6 +766,9 @@ See [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md) and [PROTOCOL.md](PROTOCOL.
|
|
|
544
766
|
| `generateAddress(input)` | Generate a protocol-2 address with fixed 100-meter blocks and 1-meter detail. |
|
|
545
767
|
| `generateLegacyAddress(input)` | Explicitly reproduce the original protocol-1 address and grid ID. |
|
|
546
768
|
| `getDirectionsUrl(savedRecord)` | Build directions using original saved GPS coordinates, for either version. |
|
|
769
|
+
| `getDistance(pointA, pointB, options?)` | Haversine meters/kilometers from GPS, or projected distance within one exact grid ID. |
|
|
770
|
+
| `optimizeRoute(points, options?)` | Exact or approximate stop order, original indices, legs and totals for an optimized straight-line itinerary. |
|
|
771
|
+
| `ROUTE_OPTIMIZATION` | Inspect the 12-stop exact cutoff, 1000-stop limit and 100-pass heuristic work bound. |
|
|
547
772
|
| `getCountries()` / `getCountry(code)` | Select or inspect immutable country configurations. |
|
|
548
773
|
| `getProvinces(code)` | List configured provinces; country-wide grids return `[]`. |
|
|
549
774
|
| `ZAP_PROTOCOL` / `LEGACY_ZAP_PROTOCOL` | Read current or original protocol constants. |
|
|
@@ -562,7 +787,11 @@ Tests cover the requested formats, all directions, zero and sub-meter negatives,
|
|
|
562
787
|
99/100-meter boundaries, two-digit suffixes, country/province selection, unchanged
|
|
563
788
|
grid identity components, date-line rectangles, diagonal sequences, 100-meter
|
|
564
789
|
axis movements, old-address compatibility, saved GPS directions, and all centers
|
|
565
|
-
and corners of 747 configured grids.
|
|
790
|
+
and corners of 747 configured grids. Distance/itinerary tests check known
|
|
791
|
+
distances, mixed grids, endpoints, return legs, duplicate identities, explicit
|
|
792
|
+
resolvers, and exact solutions against an independent exhaustive search.
|
|
793
|
+
Heuristics are checked against the original feasible order, including directed
|
|
794
|
+
matrices. Package verification checks ESM/CommonJS
|
|
566
795
|
runtime exports and compiles both TypeScript consumer formats, including the
|
|
567
796
|
removed-option error.
|
|
568
797
|
|
|
@@ -571,11 +800,11 @@ removed-option error.
|
|
|
571
800
|
```sh
|
|
572
801
|
npm login
|
|
573
802
|
npm run pack:check
|
|
574
|
-
npm publish ./.artifacts/zukall-zap-0.
|
|
803
|
+
npm publish ./.artifacts/zukall-zap-0.3.0.tgz --access public
|
|
575
804
|
```
|
|
576
805
|
|
|
577
806
|
An npm account with permission to publish `@zukall/zap` is required. Building and
|
|
578
|
-
packing do not publish the package. Package version is `0.
|
|
807
|
+
packing do not publish the package. Package version is `0.3.0`; current protocol
|
|
579
808
|
version is `2`, while preserved legacy results remain version `1`.
|
|
580
809
|
|
|
581
810
|
## License
|
|
Binary file
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Coordinates, DistanceMethod, DistanceOptions, DistanceResult, LocationInput, LocationPoint, ResolvedDistanceOptions } from "./types.js";
|
|
2
|
+
export declare function getDistanceMethod(method: DistanceMethod | undefined): DistanceMethod;
|
|
3
|
+
/** Geographic great-circle surface distance, using the existing spherical Earth radius. */
|
|
4
|
+
export declare function haversineMeters(pointA: Coordinates, pointB: Coordinates): number;
|
|
5
|
+
export declare function requireFiniteDistance(meters: number): number;
|
|
6
|
+
export declare function getDistance(pointA: LocationPoint, pointB: LocationPoint, options?: DistanceOptions): DistanceResult;
|
|
7
|
+
export declare function getDistance(pointA: LocationInput, pointB: LocationInput, options: ResolvedDistanceOptions): Promise<DistanceResult>;
|
|
8
|
+
export declare function getDistance(pointA: LocationPoint, pointB: LocationPoint, options: DistanceOptions | ResolvedDistanceOptions): DistanceResult | Promise<DistanceResult>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getDistanceMethod = getDistanceMethod;
|
|
4
|
+
exports.haversineMeters = haversineMeters;
|
|
5
|
+
exports.requireFiniteDistance = requireFiniteDistance;
|
|
6
|
+
exports.getDistance = getDistance;
|
|
7
|
+
const protocol_js_1 = require("./protocol.js");
|
|
8
|
+
const locations_js_1 = require("./locations.js");
|
|
9
|
+
function getDistanceMethod(method) {
|
|
10
|
+
if (method !== undefined && method !== "haversine" && method !== "projected") {
|
|
11
|
+
throw new TypeError("method must be haversine or projected.");
|
|
12
|
+
}
|
|
13
|
+
return method ?? "haversine";
|
|
14
|
+
}
|
|
15
|
+
/** Geographic great-circle surface distance, using the existing spherical Earth radius. */
|
|
16
|
+
function haversineMeters(pointA, pointB) {
|
|
17
|
+
const radians = (degrees) => degrees * Math.PI / 180;
|
|
18
|
+
const latitudeA = radians(pointA.latitude);
|
|
19
|
+
const latitudeB = radians(pointB.latitude);
|
|
20
|
+
let longitudeDifference = pointB.longitude - pointA.longitude;
|
|
21
|
+
if (longitudeDifference > 180)
|
|
22
|
+
longitudeDifference -= 360;
|
|
23
|
+
if (longitudeDifference < -180)
|
|
24
|
+
longitudeDifference += 360;
|
|
25
|
+
const cosineA = Math.abs(pointA.latitude) === 90 ? 0 : Math.cos(latitudeA);
|
|
26
|
+
const cosineB = Math.abs(pointB.latitude) === 90 ? 0 : Math.cos(latitudeB);
|
|
27
|
+
const haversine = Math.sin(radians(pointB.latitude - pointA.latitude) / 2) ** 2
|
|
28
|
+
+ cosineA * cosineB * Math.sin(radians(longitudeDifference) / 2) ** 2;
|
|
29
|
+
const clamped = Math.max(0, Math.min(1, haversine));
|
|
30
|
+
return 2 * protocol_js_1.ZAP_PROTOCOL.earthRadiusMeters * Math.atan2(Math.sqrt(clamped), Math.sqrt(1 - clamped));
|
|
31
|
+
}
|
|
32
|
+
function requireFiniteDistance(meters) {
|
|
33
|
+
if (!Number.isFinite(meters) || meters < 0)
|
|
34
|
+
throw new RangeError("Distance must be finite and nonnegative.");
|
|
35
|
+
return meters === 0 ? 0 : meters;
|
|
36
|
+
}
|
|
37
|
+
function calculateDistance(points, method) {
|
|
38
|
+
const pointA = points[0];
|
|
39
|
+
const pointB = points[1];
|
|
40
|
+
const coordinatesA = (0, locations_js_1.getLocationCoordinates)(pointA);
|
|
41
|
+
const coordinatesB = (0, locations_js_1.getLocationCoordinates)(pointB);
|
|
42
|
+
if (method === "projected") {
|
|
43
|
+
const gridA = (0, locations_js_1.getProjectedPoint)(pointA);
|
|
44
|
+
const gridB = (0, locations_js_1.getProjectedPoint)(pointB);
|
|
45
|
+
if (gridA.gridId !== gridB.gridId) {
|
|
46
|
+
throw new RangeError("Projected distance requires exactly the same gridId.");
|
|
47
|
+
}
|
|
48
|
+
const meters = requireFiniteDistance(Math.hypot(gridB.northingMeters - gridA.northingMeters, gridB.eastingMeters - gridA.eastingMeters));
|
|
49
|
+
return { method, meters, kilometers: meters / 1000, gridId: gridA.gridId };
|
|
50
|
+
}
|
|
51
|
+
const meters = requireFiniteDistance(haversineMeters(coordinatesA, coordinatesB));
|
|
52
|
+
return { method, meters, kilometers: meters / 1000 };
|
|
53
|
+
}
|
|
54
|
+
/** GPS by default; supplying a resolver returns a Promise and never silently geocodes text. */
|
|
55
|
+
function getDistance(pointA, pointB, options = {}) {
|
|
56
|
+
(0, locations_js_1.validateLocationOptions)(options);
|
|
57
|
+
const method = getDistanceMethod(options.method);
|
|
58
|
+
const resolved = (0, locations_js_1.resolveLocationPoints)([pointA, pointB], options.resolveAddress);
|
|
59
|
+
return resolved instanceof Promise
|
|
60
|
+
? resolved.then(points => calculateDistance(points, method)) : calculateDistance(resolved, method);
|
|
61
|
+
}
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { generateAddress } from "./address.js";
|
|
2
2
|
import { generateLegacyAddress } from "./legacy-address.js";
|
|
3
3
|
import { getDirectionsUrl } from "./directions.js";
|
|
4
|
+
import { getDistance } from "./distance.js";
|
|
5
|
+
import { optimizeRoute, ROUTE_OPTIMIZATION } from "./route.js";
|
|
4
6
|
import { getCountries, getCountry, getProvinces } from "./country.js";
|
|
5
7
|
import { ZAP_PROTOCOL, LEGACY_ZAP_PROTOCOL } from "./protocol.js";
|
|
6
|
-
export { generateAddress, generateLegacyAddress, getDirectionsUrl, getCountries, getCountry, getProvinces, ZAP_PROTOCOL, LEGACY_ZAP_PROTOCOL, };
|
|
7
|
-
export type { AddressInput, AddressResult, AddressEncoding, LegacyAddressInput, LegacyAddressResult, Coordinates, Corners, GridInput, Country, CountryOption, Province, ProvinceCountry, ProvinceOption, } from "./types.js";
|
|
8
|
+
export { generateAddress, generateLegacyAddress, getDirectionsUrl, getDistance, optimizeRoute, ROUTE_OPTIMIZATION, getCountries, getCountry, getProvinces, ZAP_PROTOCOL, LEGACY_ZAP_PROTOCOL, };
|
|
9
|
+
export type { AddressInput, AddressResult, AddressEncoding, LegacyAddressInput, LegacyAddressResult, Coordinates, Corners, GridInput, Country, CountryOption, Province, ProvinceCountry, ProvinceOption, SavedLocationPoint, LocationPoint, LocationInput, AddressResolver, DistanceMethod, RouteDistanceMethod, DistanceOptions, ResolvedDistanceOptions, DistanceResult, DistanceMatrix, RouteOptions, ResolvedRouteOptions, RouteLeg, RouteResult, } from "./types.js";
|
|
8
10
|
/** Named namespace for applications that refer to the protocol as `zap`. */
|
|
9
11
|
export declare const zap: Readonly<{
|
|
10
12
|
protocol: Readonly<{
|
|
@@ -21,6 +23,13 @@ export declare const zap: Readonly<{
|
|
|
21
23
|
generateAddress: typeof generateAddress;
|
|
22
24
|
generateLegacyAddress: typeof generateLegacyAddress;
|
|
23
25
|
getDirectionsUrl: typeof getDirectionsUrl;
|
|
26
|
+
getDistance: typeof getDistance;
|
|
27
|
+
optimizeRoute: typeof optimizeRoute;
|
|
28
|
+
routeOptimization: Readonly<{
|
|
29
|
+
readonly exactMaxStops: 12;
|
|
30
|
+
readonly maxStops: 1000;
|
|
31
|
+
readonly twoOptMaxPasses: 100;
|
|
32
|
+
}>;
|
|
24
33
|
getCountries: typeof getCountries;
|
|
25
34
|
getCountry: typeof getCountry;
|
|
26
35
|
getProvinces: typeof getProvinces;
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.zap = exports.LEGACY_ZAP_PROTOCOL = exports.ZAP_PROTOCOL = exports.getProvinces = exports.getCountry = exports.getCountries = exports.getDirectionsUrl = exports.generateLegacyAddress = exports.generateAddress = void 0;
|
|
3
|
+
exports.zap = exports.LEGACY_ZAP_PROTOCOL = exports.ZAP_PROTOCOL = exports.getProvinces = exports.getCountry = exports.getCountries = exports.ROUTE_OPTIMIZATION = exports.optimizeRoute = exports.getDistance = exports.getDirectionsUrl = exports.generateLegacyAddress = exports.generateAddress = void 0;
|
|
4
4
|
const address_js_1 = require("./address.js");
|
|
5
5
|
Object.defineProperty(exports, "generateAddress", { enumerable: true, get: function () { return address_js_1.generateAddress; } });
|
|
6
6
|
const legacy_address_js_1 = require("./legacy-address.js");
|
|
7
7
|
Object.defineProperty(exports, "generateLegacyAddress", { enumerable: true, get: function () { return legacy_address_js_1.generateLegacyAddress; } });
|
|
8
8
|
const directions_js_1 = require("./directions.js");
|
|
9
9
|
Object.defineProperty(exports, "getDirectionsUrl", { enumerable: true, get: function () { return directions_js_1.getDirectionsUrl; } });
|
|
10
|
+
const distance_js_1 = require("./distance.js");
|
|
11
|
+
Object.defineProperty(exports, "getDistance", { enumerable: true, get: function () { return distance_js_1.getDistance; } });
|
|
12
|
+
const route_js_1 = require("./route.js");
|
|
13
|
+
Object.defineProperty(exports, "optimizeRoute", { enumerable: true, get: function () { return route_js_1.optimizeRoute; } });
|
|
14
|
+
Object.defineProperty(exports, "ROUTE_OPTIMIZATION", { enumerable: true, get: function () { return route_js_1.ROUTE_OPTIMIZATION; } });
|
|
10
15
|
const country_js_1 = require("./country.js");
|
|
11
16
|
Object.defineProperty(exports, "getCountries", { enumerable: true, get: function () { return country_js_1.getCountries; } });
|
|
12
17
|
Object.defineProperty(exports, "getCountry", { enumerable: true, get: function () { return country_js_1.getCountry; } });
|
|
@@ -20,6 +25,9 @@ exports.zap = Object.freeze({
|
|
|
20
25
|
generateAddress: address_js_1.generateAddress,
|
|
21
26
|
generateLegacyAddress: legacy_address_js_1.generateLegacyAddress,
|
|
22
27
|
getDirectionsUrl: directions_js_1.getDirectionsUrl,
|
|
28
|
+
getDistance: distance_js_1.getDistance,
|
|
29
|
+
optimizeRoute: route_js_1.optimizeRoute,
|
|
30
|
+
routeOptimization: route_js_1.ROUTE_OPTIMIZATION,
|
|
23
31
|
getCountries: country_js_1.getCountries,
|
|
24
32
|
getCountry: country_js_1.getCountry,
|
|
25
33
|
getProvinces: country_js_1.getProvinces,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AddressResolver, Coordinates, LocationInput, LocationPoint, SavedLocationPoint } from "./types.js";
|
|
2
|
+
export declare function validateLocationOptions(options: unknown): void;
|
|
3
|
+
/** Read original GPS only; never parse text, grid blocks, unit numbers, or suffixes. */
|
|
4
|
+
export declare function getLocationCoordinates(point: LocationPoint): Coordinates;
|
|
5
|
+
export declare function resolveLocationPoints(points: readonly LocationInput[], resolver: AddressResolver | undefined): LocationPoint[] | Promise<LocationPoint[]>;
|
|
6
|
+
export type ProjectedPoint = SavedLocationPoint & Readonly<{
|
|
7
|
+
gridId: string;
|
|
8
|
+
northingMeters: number;
|
|
9
|
+
eastingMeters: number;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function getProjectedPoint(point: LocationPoint): ProjectedPoint;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateLocationOptions = validateLocationOptions;
|
|
4
|
+
exports.getLocationCoordinates = getLocationCoordinates;
|
|
5
|
+
exports.resolveLocationPoints = resolveLocationPoints;
|
|
6
|
+
exports.getProjectedPoint = getProjectedPoint;
|
|
7
|
+
const grid_js_1 = require("./grid.js");
|
|
8
|
+
function validateLocationOptions(options) {
|
|
9
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
10
|
+
throw new TypeError("Options must be an object.");
|
|
11
|
+
}
|
|
12
|
+
if ("resolveAddress" in options && options.resolveAddress !== undefined && typeof options.resolveAddress !== "function") {
|
|
13
|
+
throw new TypeError("resolveAddress must be a function.");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Read original GPS only; never parse text, grid blocks, unit numbers, or suffixes. */
|
|
17
|
+
function getLocationCoordinates(point) {
|
|
18
|
+
if (!point || typeof point !== "object" || Array.isArray(point)) {
|
|
19
|
+
throw new TypeError("A latitude/longitude object or a saved record with coordinates is required.");
|
|
20
|
+
}
|
|
21
|
+
const coordinates = "coordinates" in point ? point.coordinates : point;
|
|
22
|
+
if (!coordinates || typeof coordinates !== "object" || Array.isArray(coordinates)) {
|
|
23
|
+
throw new TypeError("A saved record must contain its original GPS coordinates.");
|
|
24
|
+
}
|
|
25
|
+
(0, grid_js_1.validateCoordinates)(coordinates);
|
|
26
|
+
return { latitude: coordinates.latitude, longitude: coordinates.longitude };
|
|
27
|
+
}
|
|
28
|
+
function validateResolvedRecord(record) {
|
|
29
|
+
if (!record || typeof record !== "object" || Array.isArray(record) || !("coordinates" in record)) {
|
|
30
|
+
throw new TypeError("resolveAddress must return a saved record with original GPS coordinates.");
|
|
31
|
+
}
|
|
32
|
+
getLocationCoordinates(record);
|
|
33
|
+
return record;
|
|
34
|
+
}
|
|
35
|
+
function resolveLocationPoints(points, resolver) {
|
|
36
|
+
const checkString = (point) => {
|
|
37
|
+
if (!point.trim())
|
|
38
|
+
throw new TypeError("An address string must be nonempty.");
|
|
39
|
+
};
|
|
40
|
+
if (resolver !== undefined) {
|
|
41
|
+
// Promise.all installs rejection handlers for every asynchronous lookup.
|
|
42
|
+
return Promise.all(points.map(async (point, originalIndex) => {
|
|
43
|
+
if (typeof point === "string") {
|
|
44
|
+
checkString(point);
|
|
45
|
+
return validateResolvedRecord(await resolver(point, originalIndex));
|
|
46
|
+
}
|
|
47
|
+
getLocationCoordinates(point);
|
|
48
|
+
return point;
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
return points.map(point => {
|
|
52
|
+
if (typeof point === "string") {
|
|
53
|
+
checkString(point);
|
|
54
|
+
throw new TypeError("Address strings require resolveAddress; ZAP has no reverse decoder or automatic geocoder.");
|
|
55
|
+
}
|
|
56
|
+
getLocationCoordinates(point);
|
|
57
|
+
return point;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function getProjectedPoint(point) {
|
|
61
|
+
if (!("coordinates" in point) || !("gridId" in point) || typeof point.gridId !== "string" || !point.gridId.trim()
|
|
62
|
+
|| !("northingMeters" in point) || !Number.isFinite(point.northingMeters)
|
|
63
|
+
|| !("eastingMeters" in point) || !Number.isFinite(point.eastingMeters)) {
|
|
64
|
+
throw new TypeError("Projected distance requires saved records with a gridId and finite northingMeters/eastingMeters.");
|
|
65
|
+
}
|
|
66
|
+
return point;
|
|
67
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LocationInput, LocationPoint, ResolvedRouteOptions, RouteOptions, RouteResult } from "./types.js";
|
|
2
|
+
/** Work limits are separate from address protocol/configuration versions. */
|
|
3
|
+
export declare const ROUTE_OPTIMIZATION: Readonly<{
|
|
4
|
+
readonly exactMaxStops: 12;
|
|
5
|
+
readonly maxStops: 1000;
|
|
6
|
+
readonly twoOptMaxPasses: 100;
|
|
7
|
+
}>;
|
|
8
|
+
export declare function optimizeRoute<T extends LocationPoint>(points: readonly T[], options?: RouteOptions): RouteResult<T>;
|
|
9
|
+
export declare function optimizeRoute<T extends LocationInput>(points: readonly T[], options: ResolvedRouteOptions): Promise<RouteResult<T>>;
|
|
10
|
+
export declare function optimizeRoute<T extends LocationPoint>(points: readonly T[], options: RouteOptions | ResolvedRouteOptions): RouteResult<T> | Promise<RouteResult<T>>;
|