canvas-globe 0.1.0 → 0.1.2

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/src/viewer.js CHANGED
@@ -1,130 +1,130 @@
1
- /**
2
- * Locates the person looking at the globe, with no permission prompt, no
3
- * network call and no API key.
4
- *
5
- * The browser's IANA time zone is always available and maps to a published
6
- * coordinate, which puts the viewer within their time-zone region. That is
7
- * region-accurate, not street-accurate: call `locateViewerPrecise()` to offer
8
- * a GPS upgrade behind the usual permission prompt.
9
- */
10
- import { zoneTable, zoneAliases } from "./data/timezones.js";
11
-
12
- let byZone = null;
13
- let byCountry = null;
14
- let byCity = null;
15
- let aliasOf = null;
16
-
17
- const parseZones = () => {
18
- if (byZone) return;
19
- byZone = new Map();
20
- byCountry = new Map();
21
- byCity = new Map();
22
- aliasOf = new Map();
23
- for (const row of zoneTable.split(";")) {
24
- const [name, lon, lat, country] = row.split("|");
25
- const entry = { lon: Number(lon) / 100, lat: Number(lat) / 100, country, timeZone: name };
26
- byZone.set(name, entry);
27
- if (!byCountry.has(country)) byCountry.set(country, entry);
28
- // The last path segment is the zone's representative city.
29
- const city = name.slice(name.lastIndexOf("/") + 1).replace(/_/g, " ").toLowerCase();
30
- if (!byCity.has(city)) byCity.set(city, entry);
31
- }
32
- for (const link of zoneAliases.split(";")) {
33
- const [alias, target] = link.split(">");
34
- if (alias) aliasOf.set(alias, target);
35
- }
36
- };
37
-
38
- /** Published coordinate for an IANA zone name, resolving legacy aliases. */
39
- export function timeZoneLocation(name) {
40
- if (!name) return null;
41
- parseZones();
42
- return byZone.get(name) || byZone.get(aliasOf.get(name)) || null;
43
- }
44
-
45
- /** Representative coordinate for an ISO 3166-1 alpha-2 country code. */
46
- export function countryLocation(code) {
47
- if (!code) return null;
48
- parseZones();
49
- return byCountry.get(String(code).toUpperCase()) || null;
50
- }
51
-
52
- /**
53
- * Coordinate for a city name, drawn from the time-zone table's representative
54
- * cities. Covers roughly 300 major cities with no extra payload; pass your own
55
- * gazetteer to `fromRows` when you need more.
56
- */
57
- export function placeLocation(name) {
58
- if (!name) return null;
59
- parseZones();
60
- return byCity.get(String(name).trim().replace(/_/g, " ").toLowerCase()) || null;
61
- }
62
-
63
- const currentZone = () => {
64
- try {
65
- return Intl.DateTimeFormat().resolvedOptions().timeZone;
66
- } catch {
67
- return null;
68
- }
69
- };
70
-
71
- const localeCountry = () => {
72
- const tags = (typeof navigator !== "undefined" && (navigator.languages || [navigator.language])) || [];
73
- for (const tag of tags) {
74
- if (!tag) continue;
75
- const region = String(tag).split("-")[1];
76
- if (region && /^[A-Za-z]{2}$/.test(region)) return region.toUpperCase();
77
- }
78
- return null;
79
- };
80
-
81
- /**
82
- * Best-effort viewer location, synchronously. Returns null only when the
83
- * environment exposes neither a time zone nor a locale region.
84
- *
85
- * `accuracyMeters` is null here because a time zone says nothing about where
86
- * inside it you are: the caller is expected to derive a radius from the
87
- * region itself.
88
- */
89
- export function locateViewer() {
90
- const timeZone = currentZone();
91
- const zone = timeZoneLocation(timeZone);
92
- if (zone) return { ...zone, timeZone, source: "timezone", accuracy: "region", accuracyMeters: null };
93
- const country = countryLocation(localeCountry());
94
- if (country) return { ...country, timeZone, source: "locale", accuracy: "country", accuracyMeters: null };
95
- return null;
96
- }
97
-
98
- /**
99
- * Resolves to the precise coordinate if the viewer grants permission, and to
100
- * the time-zone estimate otherwise. Never rejects.
101
- */
102
- export function locateViewerPrecise({ timeout = 10000, maximumAge = 60000, enableHighAccuracy = true } = {}) {
103
- const fallback = locateViewer();
104
- const geo = typeof navigator !== "undefined" && navigator.geolocation;
105
- if (!geo) return Promise.resolve(fallback);
106
- return new Promise((resolve) => {
107
- let settled = false;
108
- const done = (value) => {
109
- if (settled) return;
110
- settled = true;
111
- resolve(value);
112
- };
113
- setTimeout(() => done(fallback), timeout + 500);
114
- geo.getCurrentPosition(
115
- (pos) =>
116
- done({
117
- lat: pos.coords.latitude,
118
- lon: pos.coords.longitude,
119
- timeZone: currentZone(),
120
- country: fallback?.country ?? null,
121
- source: "geolocation",
122
- accuracy: "precise",
123
- // Metres, as reported by the device. Wi-Fi/IP fixes can be tens of km.
124
- accuracyMeters: pos.coords.accuracy ?? null,
125
- }),
126
- () => done(fallback),
127
- { timeout, maximumAge, enableHighAccuracy },
128
- );
129
- });
130
- }
1
+ /**
2
+ * Locates the person looking at the globe, with no permission prompt, no
3
+ * network call and no API key.
4
+ *
5
+ * The browser's IANA time zone is always available and maps to a published
6
+ * coordinate, which puts the viewer within their time-zone region. That is
7
+ * region-accurate, not street-accurate: call `locateViewerPrecise()` to offer
8
+ * a GPS upgrade behind the usual permission prompt.
9
+ */
10
+ import { zoneTable, zoneAliases } from "./data/timezones.js";
11
+
12
+ let byZone = null;
13
+ let byCountry = null;
14
+ let byCity = null;
15
+ let aliasOf = null;
16
+
17
+ const parseZones = () => {
18
+ if (byZone) return;
19
+ byZone = new Map();
20
+ byCountry = new Map();
21
+ byCity = new Map();
22
+ aliasOf = new Map();
23
+ for (const row of zoneTable.split(";")) {
24
+ const [name, lon, lat, country] = row.split("|");
25
+ const entry = { lon: Number(lon) / 100, lat: Number(lat) / 100, country, timeZone: name };
26
+ byZone.set(name, entry);
27
+ if (!byCountry.has(country)) byCountry.set(country, entry);
28
+ // The last path segment is the zone's representative city.
29
+ const city = name.slice(name.lastIndexOf("/") + 1).replace(/_/g, " ").toLowerCase();
30
+ if (!byCity.has(city)) byCity.set(city, entry);
31
+ }
32
+ for (const link of zoneAliases.split(";")) {
33
+ const [alias, target] = link.split(">");
34
+ if (alias) aliasOf.set(alias, target);
35
+ }
36
+ };
37
+
38
+ /** Published coordinate for an IANA zone name, resolving legacy aliases. */
39
+ export function timeZoneLocation(name) {
40
+ if (!name) return null;
41
+ parseZones();
42
+ return byZone.get(name) || byZone.get(aliasOf.get(name)) || null;
43
+ }
44
+
45
+ /** Representative coordinate for an ISO 3166-1 alpha-2 country code. */
46
+ export function countryLocation(code) {
47
+ if (!code) return null;
48
+ parseZones();
49
+ return byCountry.get(String(code).toUpperCase()) || null;
50
+ }
51
+
52
+ /**
53
+ * Coordinate for a city name, drawn from the time-zone table's representative
54
+ * cities. Covers roughly 300 major cities with no extra payload; pass your own
55
+ * gazetteer to `fromRows` when you need more.
56
+ */
57
+ export function placeLocation(name) {
58
+ if (!name) return null;
59
+ parseZones();
60
+ return byCity.get(String(name).trim().replace(/_/g, " ").toLowerCase()) || null;
61
+ }
62
+
63
+ const currentZone = () => {
64
+ try {
65
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
66
+ } catch {
67
+ return null;
68
+ }
69
+ };
70
+
71
+ const localeCountry = () => {
72
+ const tags = (typeof navigator !== "undefined" && (navigator.languages || [navigator.language])) || [];
73
+ for (const tag of tags) {
74
+ if (!tag) continue;
75
+ const region = String(tag).split("-")[1];
76
+ if (region && /^[A-Za-z]{2}$/.test(region)) return region.toUpperCase();
77
+ }
78
+ return null;
79
+ };
80
+
81
+ /**
82
+ * Best-effort viewer location, synchronously. Returns null only when the
83
+ * environment exposes neither a time zone nor a locale region.
84
+ *
85
+ * `accuracyMeters` is null here because a time zone says nothing about where
86
+ * inside it you are: the caller is expected to derive a radius from the
87
+ * region itself.
88
+ */
89
+ export function locateViewer() {
90
+ const timeZone = currentZone();
91
+ const zone = timeZoneLocation(timeZone);
92
+ if (zone) return { ...zone, timeZone, source: "timezone", accuracy: "region", accuracyMeters: null };
93
+ const country = countryLocation(localeCountry());
94
+ if (country) return { ...country, timeZone, source: "locale", accuracy: "country", accuracyMeters: null };
95
+ return null;
96
+ }
97
+
98
+ /**
99
+ * Resolves to the precise coordinate if the viewer grants permission, and to
100
+ * the time-zone estimate otherwise. Never rejects.
101
+ */
102
+ export function locateViewerPrecise({ timeout = 10000, maximumAge = 60000, enableHighAccuracy = true } = {}) {
103
+ const fallback = locateViewer();
104
+ const geo = typeof navigator !== "undefined" && navigator.geolocation;
105
+ if (!geo) return Promise.resolve(fallback);
106
+ return new Promise((resolve) => {
107
+ let settled = false;
108
+ const done = (value) => {
109
+ if (settled) return;
110
+ settled = true;
111
+ resolve(value);
112
+ };
113
+ setTimeout(() => done(fallback), timeout + 500);
114
+ geo.getCurrentPosition(
115
+ (pos) =>
116
+ done({
117
+ lat: pos.coords.latitude,
118
+ lon: pos.coords.longitude,
119
+ timeZone: currentZone(),
120
+ country: fallback?.country ?? null,
121
+ source: "geolocation",
122
+ accuracy: "precise",
123
+ // Metres, as reported by the device. Wi-Fi/IP fixes can be tens of km.
124
+ accuracyMeters: pos.coords.accuracy ?? null,
125
+ }),
126
+ () => done(fallback),
127
+ { timeout, maximumAge, enableHighAccuracy },
128
+ );
129
+ });
130
+ }
@@ -1,41 +1,41 @@
1
- import type { Arc, CountryShape, GeoGlobe, GeoGlobeOptions, Marker, ClusterMarker, FlyToOptions } from "./index.js";
2
-
3
- export interface GeoGlobeEventMap {
4
- "geo-hover": CustomEvent<{ marker: Marker | ClusterMarker | null; pos: { x: number; y: number } | null }>;
5
- "geo-click": CustomEvent<{ marker: Marker | ClusterMarker; pos: { x: number; y: number } }>;
6
- "geo-country-hover": CustomEvent<{ country: CountryShape | null; pos: { x: number; y: number } | null }>;
7
- "geo-country-click": CustomEvent<{ country: CountryShape; pos: { x: number; y: number } }>;
8
- "geo-render": CustomEvent<{ globe: GeoGlobe }>;
9
- }
10
-
11
-
12
- declare class GeoGlobeElementInstance extends HTMLElement {
13
- /** Underlying instance; available once the element is connected. */
14
- globe: GeoGlobe | null;
15
- markers: Marker[];
16
- arcs: Arc[];
17
- set options(value: Partial<GeoGlobeOptions>);
18
- flyTo(lon: number, lat: number, opts?: FlyToOptions): this;
19
- fitTo(bounds: [number, number, number, number], opts?: FlyToOptions & { padding?: number }): this;
20
- snapshot(type?: string, quality?: number): string | undefined;
21
- addEventListener<K extends keyof GeoGlobeEventMap>(
22
- type: K,
23
- listener: (this: GeoGlobeElementInstance, ev: GeoGlobeEventMap[K]) => unknown,
24
- options?: boolean | AddEventListenerOptions
25
- ): void;
26
- addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
27
- }
28
-
29
- export type GeoGlobeElement = GeoGlobeElementInstance;
30
-
31
- /** The element class, or null when there is no DOM. */
32
- export declare const GeoGlobeElement: (new () => GeoGlobeElementInstance) | null;
33
-
34
- /** Registers `<geo-globe>`. Returns null when there is no DOM. */
35
- export declare function defineGeoGlobe(tag?: string): (new () => GeoGlobeElementInstance) | null;
36
-
37
- declare global {
38
- interface HTMLElementTagNameMap {
39
- "geo-globe": GeoGlobeElementInstance;
40
- }
41
- }
1
+ import type { Arc, CountryShape, GeoGlobe, GeoGlobeOptions, Marker, ClusterMarker, FlyToOptions } from "./index.js";
2
+
3
+ export interface GeoGlobeEventMap {
4
+ "geo-hover": CustomEvent<{ marker: Marker | ClusterMarker | null; pos: { x: number; y: number } | null }>;
5
+ "geo-click": CustomEvent<{ marker: Marker | ClusterMarker; pos: { x: number; y: number } }>;
6
+ "geo-country-hover": CustomEvent<{ country: CountryShape | null; pos: { x: number; y: number } | null }>;
7
+ "geo-country-click": CustomEvent<{ country: CountryShape; pos: { x: number; y: number } }>;
8
+ "geo-render": CustomEvent<{ globe: GeoGlobe }>;
9
+ }
10
+
11
+
12
+ declare class GeoGlobeElementInstance extends HTMLElement {
13
+ /** Underlying instance; available once the element is connected. */
14
+ globe: GeoGlobe | null;
15
+ markers: Marker[];
16
+ arcs: Arc[];
17
+ set options(value: Partial<GeoGlobeOptions>);
18
+ flyTo(lon: number, lat: number, opts?: FlyToOptions): this;
19
+ fitTo(bounds: [number, number, number, number], opts?: FlyToOptions & { padding?: number }): this;
20
+ snapshot(type?: string, quality?: number): string | undefined;
21
+ addEventListener<K extends keyof GeoGlobeEventMap>(
22
+ type: K,
23
+ listener: (this: GeoGlobeElementInstance, ev: GeoGlobeEventMap[K]) => unknown,
24
+ options?: boolean | AddEventListenerOptions
25
+ ): void;
26
+ addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
27
+ }
28
+
29
+ export type GeoGlobeElement = GeoGlobeElementInstance;
30
+
31
+ /** The element class, or null when there is no DOM. */
32
+ export declare const GeoGlobeElement: (new () => GeoGlobeElementInstance) | null;
33
+
34
+ /** Registers `<geo-globe>`. Returns null when there is no DOM. */
35
+ export declare function defineGeoGlobe(tag?: string): (new () => GeoGlobeElementInstance) | null;
36
+
37
+ declare global {
38
+ interface HTMLElementTagNameMap {
39
+ "geo-globe": GeoGlobeElementInstance;
40
+ }
41
+ }