@mailwoman/timezone-lookup 4.15.1
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/README.md +35 -0
- package/out/build.d.ts +15 -0
- package/out/build.d.ts.map +1 -0
- package/out/build.js +52 -0
- package/out/build.js.map +1 -0
- package/out/cli.d.ts +13 -0
- package/out/cli.d.ts.map +1 -0
- package/out/cli.js +52 -0
- package/out/cli.js.map +1 -0
- package/out/index.d.ts +37 -0
- package/out/index.d.ts.map +1 -0
- package/out/index.js +91 -0
- package/out/index.js.map +1 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @mailwoman/timezone-lookup
|
|
2
|
+
|
|
3
|
+
Coordinate → IANA timezone, server-side. Point-in-polygon over
|
|
4
|
+
[timezone-boundary-builder](https://github.com/evansiroky/timezone-boundary-builder) polygons in a
|
|
5
|
+
`node:sqlite` DB; the UTC offset comes from `Intl` (no tz-database dependency). An
|
|
6
|
+
[`@mailwoman/annotations`](../annotations) `Annotator`.
|
|
7
|
+
|
|
8
|
+
## Build the DB
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
# from the downloaded combined-with-oceans.json (tz-boundary-builder release)
|
|
12
|
+
npx @mailwoman/timezone-lookup build --geojson combined-with-oceans.json --out timezone.db
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Look up
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx @mailwoman/timezone-lookup --db timezone.db 40.7128 -74.0060
|
|
19
|
+
# {"timezone":"America/New_York","offsetSec":-18000}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Library
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { TimezoneLookup, makeTimezoneAnnotator } from "@mailwoman/timezone-lookup"
|
|
26
|
+
|
|
27
|
+
const lookup = new TimezoneLookup({ databasePath: "timezone.db" })
|
|
28
|
+
lookup.find(35.6762, 139.6503) // "Asia/Tokyo"
|
|
29
|
+
|
|
30
|
+
const annotator = makeTimezoneAnnotator(lookup) // fills AnnotationSet.timezone
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Server-side only — the polygon PIP runs over `node:sqlite`. A browser/WASM build is a follow-up.
|
|
34
|
+
|
|
35
|
+
Data: timezone-boundary-builder (ODbL). Attribution + share-alike apply to the built DB.
|
package/out/build.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Build the timezone polygon DB from a timezone-boundary-builder GeoJSON
|
|
7
|
+
* (`combined-with-oceans.json`, features of `{ properties: { tzid }, geometry:
|
|
8
|
+
* Polygon|MultiPolygon }`). One row per feature: the tzid, a bounding box (for the lookup's
|
|
9
|
+
* prefilter), and the geometry normalized to MultiPolygon coordinates as JSON.
|
|
10
|
+
*/
|
|
11
|
+
/** Read the GeoJSON at `geojsonPath` and write the polygon DB to `dbPath` (overwriting its table). */
|
|
12
|
+
export declare function buildTimezoneDb(geojsonPath: string, dbPath: string): {
|
|
13
|
+
features: number;
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=build.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../build.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAWH,sGAAsG;AACtG,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAwCzF"}
|
package/out/build.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Build the timezone polygon DB from a timezone-boundary-builder GeoJSON
|
|
7
|
+
* (`combined-with-oceans.json`, features of `{ properties: { tzid }, geometry:
|
|
8
|
+
* Polygon|MultiPolygon }`). One row per feature: the tzid, a bounding box (for the lookup's
|
|
9
|
+
* prefilter), and the geometry normalized to MultiPolygon coordinates as JSON.
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { DatabaseSync } from "node:sqlite";
|
|
13
|
+
/** Read the GeoJSON at `geojsonPath` and write the polygon DB to `dbPath` (overwriting its table). */
|
|
14
|
+
export function buildTimezoneDb(geojsonPath, dbPath) {
|
|
15
|
+
const data = JSON.parse(readFileSync(geojsonPath, "utf8"));
|
|
16
|
+
const db = new DatabaseSync(dbPath);
|
|
17
|
+
db.exec("DROP TABLE IF EXISTS timezone_polygons");
|
|
18
|
+
db.exec(`CREATE TABLE timezone_polygons (tzid TEXT NOT NULL, minLat REAL, maxLat REAL, minLon REAL, maxLon REAL, geom TEXT NOT NULL)`);
|
|
19
|
+
const insert = db.prepare("INSERT INTO timezone_polygons (tzid, minLat, maxLat, minLon, maxLon, geom) VALUES (?,?,?,?,?,?)");
|
|
20
|
+
db.exec("BEGIN");
|
|
21
|
+
for (const feature of data.features) {
|
|
22
|
+
const polygons = feature.geometry.type === "Polygon"
|
|
23
|
+
? [feature.geometry.coordinates]
|
|
24
|
+
: feature.geometry.coordinates;
|
|
25
|
+
let minLat = 90;
|
|
26
|
+
let maxLat = -90;
|
|
27
|
+
let minLon = 180;
|
|
28
|
+
let maxLon = -180;
|
|
29
|
+
for (const polygon of polygons) {
|
|
30
|
+
for (const ring of polygon) {
|
|
31
|
+
for (const point of ring) {
|
|
32
|
+
const lon = point[0];
|
|
33
|
+
const lat = point[1];
|
|
34
|
+
if (lat < minLat)
|
|
35
|
+
minLat = lat;
|
|
36
|
+
if (lat > maxLat)
|
|
37
|
+
maxLat = lat;
|
|
38
|
+
if (lon < minLon)
|
|
39
|
+
minLon = lon;
|
|
40
|
+
if (lon > maxLon)
|
|
41
|
+
maxLon = lon;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
insert.run(feature.properties.tzid, minLat, maxLat, minLon, maxLon, JSON.stringify(polygons));
|
|
46
|
+
}
|
|
47
|
+
db.exec("COMMIT");
|
|
48
|
+
db.exec("CREATE INDEX idx_tz_bbox ON timezone_polygons (minLat, maxLat, minLon, maxLon)");
|
|
49
|
+
db.close();
|
|
50
|
+
return { features: data.features.length };
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=build.js.map
|
package/out/build.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.js","sourceRoot":"","sources":["../build.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAQ1C,sGAAsG;AACtG,MAAM,UAAU,eAAe,CAAC,WAAmB,EAAE,MAAc;IAClE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAoC,CAAA;IAC7F,MAAM,EAAE,GAAG,IAAI,YAAY,CAAC,MAAM,CAAC,CAAA;IACnC,EAAE,CAAC,IAAI,CAAC,wCAAwC,CAAC,CAAA;IACjD,EAAE,CAAC,IAAI,CACN,6HAA6H,CAC7H,CAAA;IACD,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CACxB,iGAAiG,CACjG,CAAA;IAED,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAChB,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QACrC,MAAM,QAAQ,GACb,OAAO,CAAC,QAAQ,CAAC,IAAI,KAAK,SAAS;YAClC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,WAA2B,CAAC;YAChD,CAAC,CAAE,OAAO,CAAC,QAAQ,CAAC,WAA8B,CAAA;QAEpD,IAAI,MAAM,GAAG,EAAE,CAAA;QACf,IAAI,MAAM,GAAG,CAAC,EAAE,CAAA;QAChB,IAAI,MAAM,GAAG,GAAG,CAAA;QAChB,IAAI,MAAM,GAAG,CAAC,GAAG,CAAA;QACjB,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAChC,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;gBAC5B,KAAK,MAAM,KAAK,IAAI,IAAI,EAAE,CAAC;oBAC1B,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAE,CAAA;oBACrB,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAE,CAAA;oBACrB,IAAI,GAAG,GAAG,MAAM;wBAAE,MAAM,GAAG,GAAG,CAAA;oBAC9B,IAAI,GAAG,GAAG,MAAM;wBAAE,MAAM,GAAG,GAAG,CAAA;oBAC9B,IAAI,GAAG,GAAG,MAAM;wBAAE,MAAM,GAAG,GAAG,CAAA;oBAC9B,IAAI,GAAG,GAAG,MAAM;wBAAE,MAAM,GAAG,GAAG,CAAA;gBAC/B,CAAC;YACF,CAAC;QACF,CAAC;QACD,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC9F,CAAC;IACD,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACjB,EAAE,CAAC,IAAI,CAAC,gFAAgF,CAAC,CAAA;IACzF,EAAE,CAAC,KAAK,EAAE,CAAA;IACV,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAA;AAC1C,CAAC"}
|
package/out/cli.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @copyright Sister Software
|
|
4
|
+
* @license AGPL-3.0
|
|
5
|
+
* @author Teffen Ellis, et al.
|
|
6
|
+
*
|
|
7
|
+
* `mailwoman-timezone` — build the polygon DB, or look up a coordinate's IANA timezone.
|
|
8
|
+
*
|
|
9
|
+
* Mailwoman-timezone build --geojson combined-with-oceans.json --out timezone.db mailwoman-timezone
|
|
10
|
+
* --db timezone.db 40.7128 -74.0060
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=cli.d.ts.map
|
package/out/cli.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;GASG"}
|
package/out/cli.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @copyright Sister Software
|
|
4
|
+
* @license AGPL-3.0
|
|
5
|
+
* @author Teffen Ellis, et al.
|
|
6
|
+
*
|
|
7
|
+
* `mailwoman-timezone` — build the polygon DB, or look up a coordinate's IANA timezone.
|
|
8
|
+
*
|
|
9
|
+
* Mailwoman-timezone build --geojson combined-with-oceans.json --out timezone.db mailwoman-timezone
|
|
10
|
+
* --db timezone.db 40.7128 -74.0060
|
|
11
|
+
*/
|
|
12
|
+
import { parseArgs } from "node:util";
|
|
13
|
+
import { buildTimezoneDb } from "./build.js";
|
|
14
|
+
import { offsetSecForTimezone, TimezoneLookup } from "./index.js";
|
|
15
|
+
if (process.argv[2] === "build") {
|
|
16
|
+
const { values } = parseArgs({
|
|
17
|
+
args: process.argv.slice(3),
|
|
18
|
+
options: { geojson: { type: "string" }, out: { type: "string" } },
|
|
19
|
+
});
|
|
20
|
+
if (!values.geojson || !values.out) {
|
|
21
|
+
console.error("Usage: mailwoman-timezone build --geojson <path> --out <db>");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const { features } = buildTimezoneDb(values.geojson, values.out);
|
|
25
|
+
console.error(`built ${values.out} (${features} features)`);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
// Hand-parse so negative longitudes (which look like options to parseArgs) work as positionals.
|
|
29
|
+
const args = process.argv.slice(2);
|
|
30
|
+
let databasePath;
|
|
31
|
+
const coords = [];
|
|
32
|
+
for (let i = 0; i < args.length; i++) {
|
|
33
|
+
if (args[i] === "--db")
|
|
34
|
+
databasePath = args[++i];
|
|
35
|
+
else {
|
|
36
|
+
const n = Number(args[i]);
|
|
37
|
+
if (Number.isFinite(n))
|
|
38
|
+
coords.push(n);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const lat = coords[0];
|
|
42
|
+
const lon = coords[1];
|
|
43
|
+
if (!databasePath || lat == null || lon == null) {
|
|
44
|
+
console.error("Usage: mailwoman-timezone --db <db> <lat> <lon>");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const lookup = new TimezoneLookup({ databasePath });
|
|
48
|
+
const tzid = lookup.find(lat, lon);
|
|
49
|
+
console.log(JSON.stringify({ timezone: tzid, offsetSec: tzid ? offsetSecForTimezone(tzid) : null }));
|
|
50
|
+
lookup.close();
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=cli.js.map
|
package/out/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;GASG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AACrC,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEjE,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,OAAO,EAAE,CAAC;IACjC,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;QAC5B,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;QAC3B,OAAO,EAAE,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;KACjE,CAAC,CAAA;IACF,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;QACpC,OAAO,CAAC,KAAK,CAAC,6DAA6D,CAAC,CAAA;QAC5E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAChB,CAAC;IACD,MAAM,EAAE,QAAQ,EAAE,GAAG,eAAe,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,CAAA;IAChE,OAAO,CAAC,KAAK,CAAC,SAAS,MAAM,CAAC,GAAG,KAAK,QAAQ,YAAY,CAAC,CAAA;AAC5D,CAAC;KAAM,CAAC;IACP,gGAAgG;IAChG,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IAClC,IAAI,YAAgC,CAAA;IACpC,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM;YAAE,YAAY,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAA;aAC3C,CAAC;YACL,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;YACzB,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;gBAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACvC,CAAC;IACF,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;IACrB,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;IACrB,IAAI,CAAC,YAAY,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC;QACjD,OAAO,CAAC,KAAK,CAAC,iDAAiD,CAAC,CAAA;QAChE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAChB,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC,CAAA;IACnD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;IAClC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;IACpG,MAAM,CAAC,KAAK,EAAE,CAAA;AACf,CAAC"}
|
package/out/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `@mailwoman/timezone-lookup` — coordinate → IANA timezone, server-side. Point-in-polygon over the
|
|
7
|
+
* timezone-boundary-builder polygons stored in a `node:sqlite` DB (bbox-prefilter + ray-cast),
|
|
8
|
+
* mirroring the resolver's PIP pattern. The UTC offset comes from `Intl.DateTimeFormat` — no tz
|
|
9
|
+
* database dependency. Build the DB with `mailwoman-timezone build` (see `./build.ts`).
|
|
10
|
+
*/
|
|
11
|
+
import type { Annotator } from "@mailwoman/annotations";
|
|
12
|
+
import { DatabaseSync } from "node:sqlite";
|
|
13
|
+
/** Normalized geometry: an array of polygons, each `[outerRing, ...holes]`, each ring
|
|
14
|
+
`[[lon,lat],…]`. */
|
|
15
|
+
export type MultiPolygonCoords = number[][][][];
|
|
16
|
+
/** Inside any polygon of a (multi)polygon feature. */
|
|
17
|
+
export declare function pointInMultiPolygon(lon: number, lat: number, polygons: MultiPolygonCoords): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* The current UTC offset (seconds) for an IANA timezone, via `Intl` (no tz-db dependency). Returns
|
|
20
|
+
* `undefined` if the runtime can't resolve the zone.
|
|
21
|
+
*/
|
|
22
|
+
export declare function offsetSecForTimezone(tzid: string, date?: Date): number | undefined;
|
|
23
|
+
/** A timezone lookup over a built `node:sqlite` polygon DB. */
|
|
24
|
+
export declare class TimezoneLookup {
|
|
25
|
+
#private;
|
|
26
|
+
constructor(opts: {
|
|
27
|
+
databasePath: string;
|
|
28
|
+
} | {
|
|
29
|
+
database: DatabaseSync;
|
|
30
|
+
});
|
|
31
|
+
/** The IANA timezone id containing `(lat, lon)`, or `null` if none (shouldn't happen with oceans). */
|
|
32
|
+
find(lat: number, lon: number): string | null;
|
|
33
|
+
close(): void;
|
|
34
|
+
}
|
|
35
|
+
/** Build an `Annotator` that fills `AnnotationSet.timezone` (name + current offset) from a lookup. */
|
|
36
|
+
export declare function makeTimezoneAnnotator(lookup: TimezoneLookup): Annotator;
|
|
37
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,wBAAwB,CAAA;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE1C;mBACmB;AACnB,MAAM,MAAM,kBAAkB,GAAG,MAAM,EAAE,EAAE,EAAE,EAAE,CAAA;AAsB/C,sDAAsD;AACtD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAEnG;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,IAAiB,GAAG,MAAM,GAAG,SAAS,CAa9F;AAED,+DAA+D;AAC/D,qBAAa,cAAc;;gBAId,IAAI,EAAE;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,QAAQ,EAAE,YAAY,CAAA;KAAE;IASvE,sGAAsG;IACtG,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAQ7C,KAAK,IAAI,IAAI;CAGb;AAED,sGAAsG;AACtG,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,CAOvE"}
|
package/out/index.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `@mailwoman/timezone-lookup` — coordinate → IANA timezone, server-side. Point-in-polygon over the
|
|
7
|
+
* timezone-boundary-builder polygons stored in a `node:sqlite` DB (bbox-prefilter + ray-cast),
|
|
8
|
+
* mirroring the resolver's PIP pattern. The UTC offset comes from `Intl.DateTimeFormat` — no tz
|
|
9
|
+
* database dependency. Build the DB with `mailwoman-timezone build` (see `./build.ts`).
|
|
10
|
+
*/
|
|
11
|
+
import { DatabaseSync } from "node:sqlite";
|
|
12
|
+
/** Ray-cast point-in-ring (even-odd rule). `ring` is `[[lon, lat], …]`. */
|
|
13
|
+
function pointInRing(lon, lat, ring) {
|
|
14
|
+
let inside = false;
|
|
15
|
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
16
|
+
const xi = ring[i][0];
|
|
17
|
+
const yi = ring[i][1];
|
|
18
|
+
const xj = ring[j][0];
|
|
19
|
+
const yj = ring[j][1];
|
|
20
|
+
if (yi > lat !== yj > lat && lon < ((xj - xi) * (lat - yi)) / (yj - yi) + xi)
|
|
21
|
+
inside = !inside;
|
|
22
|
+
}
|
|
23
|
+
return inside;
|
|
24
|
+
}
|
|
25
|
+
/** Inside the outer ring and outside every hole. */
|
|
26
|
+
function pointInPolygon(lon, lat, polygon) {
|
|
27
|
+
if (!polygon[0] || !pointInRing(lon, lat, polygon[0]))
|
|
28
|
+
return false;
|
|
29
|
+
for (let i = 1; i < polygon.length; i++)
|
|
30
|
+
if (pointInRing(lon, lat, polygon[i]))
|
|
31
|
+
return false;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
/** Inside any polygon of a (multi)polygon feature. */
|
|
35
|
+
export function pointInMultiPolygon(lon, lat, polygons) {
|
|
36
|
+
return polygons.some((polygon) => pointInPolygon(lon, lat, polygon));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The current UTC offset (seconds) for an IANA timezone, via `Intl` (no tz-db dependency). Returns
|
|
40
|
+
* `undefined` if the runtime can't resolve the zone.
|
|
41
|
+
*/
|
|
42
|
+
export function offsetSecForTimezone(tzid, date = new Date()) {
|
|
43
|
+
try {
|
|
44
|
+
const parts = new Intl.DateTimeFormat("en-US", { timeZone: tzid, timeZoneName: "longOffset" }).formatToParts(date);
|
|
45
|
+
const name = parts.find((p) => p.type === "timeZoneName")?.value ?? "";
|
|
46
|
+
const match = name.match(/GMT([+-])(\d{2}):?(\d{2})?/);
|
|
47
|
+
if (!match)
|
|
48
|
+
return name === "GMT" ? 0 : undefined;
|
|
49
|
+
const sign = match[1] === "-" ? -1 : 1;
|
|
50
|
+
const hours = Number(match[2]);
|
|
51
|
+
const minutes = Number(match[3] ?? "0");
|
|
52
|
+
return sign * (hours * 3600 + minutes * 60);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** A timezone lookup over a built `node:sqlite` polygon DB. */
|
|
59
|
+
export class TimezoneLookup {
|
|
60
|
+
#db;
|
|
61
|
+
#stmt;
|
|
62
|
+
constructor(opts) {
|
|
63
|
+
this.#db = "database" in opts ? opts.database : new DatabaseSync(opts.databasePath, { readOnly: true });
|
|
64
|
+
// Candidate features whose bbox contains the point; PIP picks the exact one.
|
|
65
|
+
this.#stmt = this.#db.prepare(`SELECT tzid, geom FROM timezone_polygons
|
|
66
|
+
WHERE minLat <= ? AND maxLat >= ? AND minLon <= ? AND maxLon >= ?`);
|
|
67
|
+
}
|
|
68
|
+
/** The IANA timezone id containing `(lat, lon)`, or `null` if none (shouldn't happen with oceans). */
|
|
69
|
+
find(lat, lon) {
|
|
70
|
+
const rows = this.#stmt.all(lat, lat, lon, lon);
|
|
71
|
+
for (const row of rows) {
|
|
72
|
+
if (pointInMultiPolygon(lon, lat, JSON.parse(row.geom)))
|
|
73
|
+
return row.tzid;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
close() {
|
|
78
|
+
this.#db.close();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Build an `Annotator` that fills `AnnotationSet.timezone` (name + current offset) from a lookup. */
|
|
82
|
+
export function makeTimezoneAnnotator(lookup) {
|
|
83
|
+
return ({ lat, lon, date }) => {
|
|
84
|
+
const name = lookup.find(lat, lon);
|
|
85
|
+
if (!name)
|
|
86
|
+
return {};
|
|
87
|
+
const offsetSec = offsetSecForTimezone(name, date);
|
|
88
|
+
return { timezone: offsetSec != null ? { name, offsetSec } : { name } };
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=index.js.map
|
package/out/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mailwoman/timezone-lookup",
|
|
3
|
+
"version": "4.15.1",
|
|
4
|
+
"description": "Coordinate → IANA timezone, server-side. Point-in-polygon over timezone-boundary-builder polygons in a node:sqlite DB; UTC offset via Intl (no tz-db dependency). An @mailwoman/annotations Annotator.",
|
|
5
|
+
"license": "AGPL-3.0-only",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/sister-software/mailwoman.git",
|
|
9
|
+
"directory": "timezone-lookup"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
"./package.json": "./package.json",
|
|
14
|
+
".": "./out/index.js",
|
|
15
|
+
"./build": "./out/build.js"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@mailwoman/annotations": "4.15.1"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"out/**/*.js",
|
|
22
|
+
"out/**/*.js.map",
|
|
23
|
+
"out/**/*.d.ts",
|
|
24
|
+
"out/**/*.d.ts.map",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"bin": "./out/cli.js",
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
}
|
|
31
|
+
}
|