@magmacomputing/tempo-plugin-geo 1.2.0 → 1.4.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/dist/install.js CHANGED
@@ -274,7 +274,7 @@ function isNumeric(str) {
274
274
  return false;
275
275
  }
276
276
  }
277
- var isType, isPrimitive, isReference, isIterable, isString, isText, isNumber, RE_BIGINT_LITERAL, isInteger, isArray, isArrayLike, isObject, isDate, isRegExp, isSymbol, isSymbolFor, isSafeKey, isNullish, isUndefined, isDefined, isFunction, isCallable, isMap, isSet, isError, isLocale, isEmpty;
277
+ var isType, isPrimitive, isReference, isIterable, isString, isText, isNumber, RE_BIGINT_LITERAL, isInteger, isBoolean, isArray, isArrayLike, isObject, isDate, isRegExp, isSymbol, isSymbolFor, isSafeKey, isNullish, isUndefined, isDefined, isFunction, isCallable, isMap, isSet, isError, isLocale, isEmpty;
278
278
  var init_assertion_library = __esm({
279
279
  "../../library/dist/common/primitives/assertion.library.js"() {
280
280
  "use strict";
@@ -289,6 +289,7 @@ var init_assertion_library = __esm({
289
289
  isNumber = (obj) => Number.isFinite(obj);
290
290
  RE_BIGINT_LITERAL = /^[+-]?[0-9]+n$/;
291
291
  isInteger = (obj) => typeof obj === "bigint";
292
+ isBoolean = (obj) => typeof obj === "boolean";
292
293
  isArray = (obj) => Array.isArray(obj);
293
294
  isArrayLike = (obj) => protoType(obj) === "Object" && "length" in obj && Object.keys(obj).every((key) => key === "length" || isNumber(Number(key)));
294
295
  isObject = (obj) => isType(obj, "Object");
@@ -2774,8 +2775,8 @@ __export(sdk_exports, {
2774
2775
  __reExport(sdk_exports, sdk_star);
2775
2776
  import * as sdk_star from "@magmacomputing/tempo/plugin/sdk";
2776
2777
  import { definePlugin as originalPlugin, defineTerm as originalTerm } from "@magmacomputing/tempo/plugin/sdk";
2777
- var definePlugin = (config) => originalPlugin({ ...config, version: "1.2.0" });
2778
- var defineTerm = (config) => originalTerm({ ...config, version: "1.2.0" });
2778
+ var definePlugin = (config) => originalPlugin({ ...config, version: "1.4.0" });
2779
+ var defineTerm = (config) => originalTerm({ ...config, version: "1.4.0" });
2779
2780
 
2780
2781
  // ../../library/dist/common/runtime/mapper.library.js
2781
2782
  init_utility_library();
@@ -2794,8 +2795,13 @@ function evaluate(...values) {
2794
2795
  }
2795
2796
 
2796
2797
  // ../../library/dist/common/runtime/mapper.library.js
2798
+ init_cache_class();
2797
2799
  var MAP_KEY2 = "_magma_geo_";
2798
2800
  var DEFAULT_GEO_TTL = 24 * 60 * 60 * 1e3;
2801
+ var DEFAULT_REVERSE_GEO_ENDPOINT = "https://api.bigdatacloud.net/data/reverse-geocode-client";
2802
+ var DEFAULT_REVERSE_GEO_TIMEOUT = 3e3;
2803
+ var DEFAULT_REVERSE_GEO_MAX_BYTES = 64 * 1024;
2804
+ var DEFAULT_SPATIAL_CACHE_SIZE = 1e3;
2799
2805
  var GEO_PROPERTIES = [
2800
2806
  "latitude",
2801
2807
  "lat",
@@ -2810,13 +2816,33 @@ var GEO_PROPERTIES = [
2810
2816
  "timezone",
2811
2817
  "tz"
2812
2818
  ];
2813
- var normalizeLat = (lat) => isNumber(lat) && lat >= -90 && lat <= 90 ? Math.round(lat * 1e3) / 1e3 : void 0;
2814
- var normalizeLng = (lng) => isNumber(lng) && lng >= -180 && lng <= 180 ? Math.round(lng * 1e3) / 1e3 : void 0;
2815
- var normalizeCoords = (lat, lng) => {
2816
- const nLat = normalizeLat(lat);
2817
- const nLng = normalizeLng(lng);
2818
- return nLat !== void 0 && nLng !== void 0 ? { lat: nLat, lng: nLng } : void 0;
2819
+ var parseCoordNumber = (val) => {
2820
+ if (!isDefined(val) || isBoolean(val))
2821
+ return NaN;
2822
+ if (isString(val)) {
2823
+ const trimmed = val.trim();
2824
+ return trimmed.length > 0 ? Number(trimmed) : NaN;
2825
+ }
2826
+ return isNumber(val) ? val : NaN;
2827
+ };
2828
+ var normalizeLat = (lat, round = true) => {
2829
+ const n = parseCoordNumber(lat);
2830
+ if (!isNumber(n) || n < -90 || n > 90)
2831
+ return void 0;
2832
+ return round ? Math.round(n * 1e3) / 1e3 : n;
2833
+ };
2834
+ var normalizeLng = (lng, round = true) => {
2835
+ const n = parseCoordNumber(lng);
2836
+ if (!isNumber(n) || n < -180 || n > 180)
2837
+ return void 0;
2838
+ return round ? Math.round(n * 1e3) / 1e3 : n;
2839
+ };
2840
+ var normalizeCoords = (lat, lng, round = true) => {
2841
+ const nLat = normalizeLat(lat, round);
2842
+ const nLng = normalizeLng(lng, round);
2843
+ return isDefined(nLat) && isDefined(nLng) ? { lat: nLat, lng: nLng } : void 0;
2819
2844
  };
2845
+ var normalizeElevation = (elevation) => isNumber(elevation) ? Math.round(elevation * 1e3) / 1e3 : void 0;
2820
2846
  var resolveSphere = (sphere, lat) => {
2821
2847
  if (sphere === "north" || sphere === "south" || sphere === "equator")
2822
2848
  return sphere;
@@ -2824,35 +2850,47 @@ var resolveSphere = (sphere, lat) => {
2824
2850
  return lat > 1e-3 ? "north" : lat < -1e-3 ? "south" : "equator";
2825
2851
  return void 0;
2826
2852
  };
2827
- var coerceGeo = (input) => {
2853
+ var parseCoordinatePair = (input, normalize = true) => {
2828
2854
  if (isString(input) && input.includes(",")) {
2829
- const segments = input.split(",").map((s) => s.trim());
2830
- const parts = segments.map((s) => s === "" ? NaN : Number(s));
2831
- if (parts.length >= 2 && !isNaN(parts[0]) && !isNaN(parts[1])) {
2832
- const coords = normalizeCoords(parts[0], parts[1]);
2833
- if (coords) {
2834
- const result2 = { latitude: coords.lat, longitude: coords.lng };
2835
- const sphere2 = resolveSphere(void 0, coords.lat);
2836
- if (sphere2)
2837
- result2.sphere = sphere2;
2838
- return result2;
2855
+ const parts = input.split(",").map((s) => s.trim());
2856
+ return parts.length >= 2 ? normalizeCoords(parts[0], parts[1], normalize) : void 0;
2857
+ }
2858
+ return Array.isArray(input) && input.length >= 2 ? normalizeCoords(input[0], input[1], normalize) : void 0;
2859
+ };
2860
+ var assembleGeoConfig = (coords, meta) => {
2861
+ const lat = "lat" in coords ? coords.lat : coords.latitude;
2862
+ const lng = "lng" in coords ? coords.lng : coords.longitude;
2863
+ const nLat = normalizeLat(lat);
2864
+ const nLng = normalizeLng(lng);
2865
+ const sphere = resolveSphere(meta?.sphere, nLat ?? lat);
2866
+ const elevation = normalizeElevation(meta?.elevation);
2867
+ return {
2868
+ latitude: nLat ?? lat,
2869
+ longitude: nLng ?? lng,
2870
+ ...sphere ? { sphere } : {},
2871
+ ...isString(meta?.city) && !isEmpty(meta.city) ? { city: meta.city.trim() } : {},
2872
+ ...isString(meta?.country) && !isEmpty(meta.country) ? { country: meta.country.trim() } : {},
2873
+ ...elevation !== void 0 ? { elevation } : {},
2874
+ ...isString(meta?.timezone) && !isEmpty(meta.timezone) ? { timezone: meta.timezone.trim() } : {}
2875
+ };
2876
+ };
2877
+ var extractCustomProperties = (source, excludedKeys = GEO_PROPERTIES) => {
2878
+ const custom = {};
2879
+ if (isReference(source)) {
2880
+ for (const key of Object.keys(source)) {
2881
+ if (isSafeKey(key) && !excludedKeys.includes(key)) {
2882
+ custom[key] = source[key];
2839
2883
  }
2840
2884
  }
2841
- return void 0;
2842
2885
  }
2886
+ return custom;
2887
+ };
2888
+ var coerceGeo = (input) => {
2889
+ const pair = parseCoordinatePair(input);
2890
+ if (pair)
2891
+ return assembleGeoConfig(pair);
2843
2892
  if (isPrimitive(input))
2844
2893
  return void 0;
2845
- if (Array.isArray(input) && input.length >= 2) {
2846
- const coords = normalizeCoords(input[0], input[1]);
2847
- if (coords) {
2848
- const result2 = { latitude: coords.lat, longitude: coords.lng };
2849
- const sphere2 = resolveSphere(void 0, coords.lat);
2850
- if (sphere2)
2851
- result2.sphere = sphere2;
2852
- return result2;
2853
- }
2854
- return void 0;
2855
- }
2856
2894
  const geo = input.geo ?? input;
2857
2895
  const cfg = input.config?.geo ?? input.config;
2858
2896
  const lat = evaluate(geo?.latitude, geo?.lat, input.latitude, input.lat, cfg?.latitude, cfg?.lat);
@@ -2869,8 +2907,9 @@ var coerceGeo = (input) => {
2869
2907
  result.latitude = nLat;
2870
2908
  if (nLng !== void 0)
2871
2909
  result.longitude = nLng;
2872
- if (isNumber(elevation))
2873
- result.elevation = Math.round(elevation * 1e3) / 1e3;
2910
+ const nElevation = normalizeElevation(elevation);
2911
+ if (nElevation !== void 0)
2912
+ result.elevation = nElevation;
2874
2913
  const resolvedSphere = resolveSphere(sphere, result.latitude);
2875
2914
  if (resolvedSphere)
2876
2915
  result.sphere = resolvedSphere;
@@ -2881,12 +2920,8 @@ var coerceGeo = (input) => {
2881
2920
  if (isString(timezone))
2882
2921
  result.timezone = timezone;
2883
2922
  const explicitGeo = isReference(input.geo) ? input.geo : isReference(input.config?.geo) ? input.config.geo : void 0;
2884
- if (isReference(explicitGeo)) {
2885
- for (const key of Object.keys(explicitGeo)) {
2886
- if (isSafeKey(key) && !GEO_PROPERTIES.includes(key))
2887
- result[key] = explicitGeo[key];
2888
- }
2889
- }
2923
+ const customKeys = extractCustomProperties(explicitGeo);
2924
+ Object.assign(result, customKeys);
2890
2925
  return Object.keys(result).length > 0 ? result : void 0;
2891
2926
  };
2892
2927
  var resolveCacheKey = (keyOrOpts) => {
@@ -2895,16 +2930,25 @@ var resolveCacheKey = (keyOrOpts) => {
2895
2930
  return trimmed.startsWith(MAP_KEY2) ? trimmed : `${MAP_KEY2}:${trimmed}`;
2896
2931
  }
2897
2932
  if (keyOrOpts && isObject(keyOrOpts)) {
2933
+ const provider = keyOrOpts.provider ?? activeGeoProvider;
2934
+ const providerPrefix = provider && isString(provider.name) && !isEmpty(provider.name) ? `provider:${provider.name}` : void 0;
2898
2935
  const k = keyOrOpts.key ?? keyOrOpts.ip ?? keyOrOpts.query;
2899
- if (isString(k) && !isEmpty(k))
2900
- return `${MAP_KEY2}:${k.trim()}`;
2936
+ if (isString(k) && !isEmpty(k)) {
2937
+ const trimmedKey = k.trim();
2938
+ return providerPrefix ? `${MAP_KEY2}:${providerPrefix}:${trimmedKey}` : `${MAP_KEY2}:${trimmedKey}`;
2939
+ }
2940
+ if (providerPrefix) {
2941
+ return `${MAP_KEY2}:${providerPrefix}`;
2942
+ }
2943
+ } else if (activeGeoProvider && isString(activeGeoProvider.name) && !isEmpty(activeGeoProvider.name)) {
2944
+ return `${MAP_KEY2}:provider:${activeGeoProvider.name}`;
2901
2945
  }
2902
2946
  return MAP_KEY2;
2903
2947
  };
2904
2948
  var getStashedGeo = (keyOrOpts) => {
2905
2949
  const cacheKey = resolveCacheKey(keyOrOpts);
2906
2950
  try {
2907
- const raw = getStorage(cacheKey) ?? (typeof localStorage !== "undefined" ? localStorage.getItem(cacheKey) : void 0);
2951
+ const raw = getStorage(cacheKey) ?? (isCallable(globalThis.localStorage?.getItem) ? globalThis.localStorage.getItem(cacheKey) : void 0);
2908
2952
  if (!raw)
2909
2953
  return void 0;
2910
2954
  const parsed = isString(raw) && (raw.startsWith("{") || raw.startsWith("[")) ? JSON.parse(raw) : raw;
@@ -2914,35 +2958,23 @@ var getStashedGeo = (keyOrOpts) => {
2914
2958
  return void 0;
2915
2959
  }
2916
2960
  }
2917
- if (isString(parsed) && parsed.includes(",")) {
2918
- const parts = parsed.split(",").map((s) => parseFloat(s.trim()));
2919
- const coords = normalizeCoords(parts[0], parts[1]);
2920
- if (coords) {
2921
- return {
2922
- latitude: coords.lat,
2923
- longitude: coords.lng,
2924
- sphere: resolveSphere(void 0, coords.lat)
2925
- };
2926
- }
2927
- }
2961
+ const pair = parseCoordinatePair(parsed);
2962
+ if (pair)
2963
+ return assembleGeoConfig(pair);
2928
2964
  if (isObject(parsed)) {
2929
2965
  const rawCoords = parsed.geolocation?.coords ?? parsed.coords ?? parsed;
2930
2966
  const lat = rawCoords?.latitude ?? rawCoords?.lat;
2931
2967
  const lng = rawCoords?.longitude ?? rawCoords?.lng ?? rawCoords?.lon ?? rawCoords?.long;
2932
2968
  const coords = normalizeCoords(lat, lng);
2933
2969
  if (coords) {
2934
- const result = { latitude: coords.lat, longitude: coords.lng };
2935
2970
  const elevation = parsed.elevation ?? rawCoords.elevation;
2936
- if (isNumber(elevation))
2937
- result.elevation = Math.round(elevation * 1e3) / 1e3;
2938
- result.sphere = resolveSphere(parsed.sphere, coords.lat);
2939
- if (isString(parsed.country))
2940
- result.country = parsed.country;
2941
- if (isString(parsed.city))
2942
- result.city = parsed.city;
2943
- if (isString(parsed.timezone))
2944
- result.timezone = parsed.timezone;
2945
- return result;
2971
+ return assembleGeoConfig(coords, {
2972
+ elevation,
2973
+ sphere: parsed.sphere,
2974
+ country: parsed.country,
2975
+ city: parsed.city,
2976
+ timezone: parsed.timezone
2977
+ });
2946
2978
  }
2947
2979
  }
2948
2980
  } catch {
@@ -2958,7 +2990,15 @@ var clearStashedGeo = (keyOrOpts) => {
2958
2990
  const cacheKey = resolveCacheKey(keyOrOpts);
2959
2991
  setStorage(cacheKey, void 0);
2960
2992
  };
2993
+ var activeGeoProvider;
2994
+ function setGeoProvider(provider) {
2995
+ activeGeoProvider = provider;
2996
+ }
2997
+ function getGeoProvider() {
2998
+ return activeGeoProvider;
2999
+ }
2961
3000
  var geoLookup = async (opts = {}) => {
3001
+ const provider = opts.provider ?? activeGeoProvider;
2962
3002
  const useCache = opts.refresh !== true;
2963
3003
  if (useCache) {
2964
3004
  const stashed = getStashedGeo(opts);
@@ -2974,26 +3014,45 @@ var geoLookup = async (opts = {}) => {
2974
3014
  };
2975
3015
  }
2976
3016
  }
2977
- const { type } = getContext();
2978
3017
  let res;
2979
- switch (type) {
2980
- case CONTEXT.Browser: {
2981
- const { geoLocation: geoLocation2 } = await Promise.resolve().then(() => (init_mapper_library(), mapper_library_exports));
2982
- const browserRes = await geoLocation2(opts);
2983
- if (browserRes.error)
2984
- return { error: browserRes.error };
2985
- const lat = browserRes.coords?.latitude;
2986
- const lng = browserRes.coords?.longitude;
2987
- res = { ...browserRes, lat, lng, latitude: lat, longitude: lng };
2988
- break;
3018
+ if (provider && isFunction(provider.lookup)) {
3019
+ try {
3020
+ const providerRes = await provider.lookup(opts);
3021
+ if (providerRes) {
3022
+ res = providerRes;
3023
+ if (providerRes.error && opts.fallback === false)
3024
+ return providerRes;
3025
+ } else if (opts.fallback === false) {
3026
+ return { error: "Provider returned null and fallback is disabled" };
3027
+ }
3028
+ } catch (err) {
3029
+ if (opts.fallback === false)
3030
+ return { error: err?.message ?? "Provider lookup failed" };
2989
3031
  }
2990
- case CONTEXT.WebWorker:
2991
- case CONTEXT.NodeJS:
2992
- case CONTEXT.Deno:
2993
- default: {
2994
- const { serverGeoLocation: serverGeoLocation2 } = await Promise.resolve().then(() => (init_mapper_library2(), mapper_library_exports2));
2995
- res = await serverGeoLocation2(opts);
2996
- break;
3032
+ }
3033
+ if (!res || res.error) {
3034
+ if (opts.fallback === false && (provider || res?.error))
3035
+ return res && res.error ? res : { error: "Geolocation resolution failed and fallback is disabled" };
3036
+ const { type } = getContext();
3037
+ switch (type) {
3038
+ case CONTEXT.Browser: {
3039
+ const { geoLocation: geoLocation2 } = await Promise.resolve().then(() => (init_mapper_library(), mapper_library_exports));
3040
+ const browserRes = await geoLocation2(opts);
3041
+ if (browserRes.error)
3042
+ return { error: browserRes.error };
3043
+ const lat = browserRes.coords?.latitude;
3044
+ const lng = browserRes.coords?.longitude;
3045
+ res = { ...browserRes, lat, lng, latitude: lat, longitude: lng };
3046
+ break;
3047
+ }
3048
+ case CONTEXT.WebWorker:
3049
+ case CONTEXT.NodeJS:
3050
+ case CONTEXT.Deno:
3051
+ default: {
3052
+ const { serverGeoLocation: serverGeoLocation2 } = await Promise.resolve().then(() => (init_mapper_library2(), mapper_library_exports2));
3053
+ res = await serverGeoLocation2(opts);
3054
+ break;
3055
+ }
2997
3056
  }
2998
3057
  }
2999
3058
  const rawLat = res.latitude ?? res.lat;
@@ -3006,15 +3065,7 @@ var geoLookup = async (opts = {}) => {
3006
3065
  res.latitude = coords.lat;
3007
3066
  res.longitude = coords.lng;
3008
3067
  res.sphere = sphere;
3009
- const stashPayload = {
3010
- latitude: coords.lat,
3011
- longitude: coords.lng,
3012
- sphere,
3013
- ...isString(res.city) ? { city: res.city } : {},
3014
- ...isString(res.country) ? { country: res.country } : {},
3015
- ...isNumber(res.elevation) ? { elevation: Math.round(res.elevation * 1e3) / 1e3 } : {},
3016
- ...isString(res.timezone) ? { timezone: res.timezone } : {}
3017
- };
3068
+ const stashPayload = assembleGeoConfig(coords, res);
3018
3069
  try {
3019
3070
  const ttl = isNumber(opts.ttl) ? opts.ttl : DEFAULT_GEO_TTL;
3020
3071
  stashGeo(stashPayload, ttl, opts);
@@ -3023,14 +3074,183 @@ var geoLookup = async (opts = {}) => {
3023
3074
  }
3024
3075
  return res;
3025
3076
  };
3077
+ var requestModulePromise2 = null;
3078
+ var getRequestModule2 = () => {
3079
+ if (!requestModulePromise2)
3080
+ requestModulePromise2 = Promise.resolve().then(() => (init_request_library(), request_library_exports));
3081
+ return requestModulePromise2;
3082
+ };
3083
+ var reverseGeoCache = new BoundedCache(DEFAULT_SPATIAL_CACHE_SIZE, DEFAULT_GEO_TTL);
3084
+ var isValidGeoEndpoint = (urlStr) => {
3085
+ try {
3086
+ const parsed = new URL(urlStr);
3087
+ return parsed.protocol === "https:" || parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
3088
+ } catch {
3089
+ return false;
3090
+ }
3091
+ };
3092
+ var getSpatialCacheKey = (lat, lng, sourceOrOpts) => {
3093
+ const bucket = `${lat.toFixed(2)},${lng.toFixed(2)}`;
3094
+ if (isString(sourceOrOpts) && !isEmpty(sourceOrOpts)) {
3095
+ return `${bucket}:${sourceOrOpts.trim()}`;
3096
+ }
3097
+ if (isObject(sourceOrOpts)) {
3098
+ const hasExplicitProvider = Object.hasOwn(sourceOrOpts, "provider");
3099
+ const provider = hasExplicitProvider ? sourceOrOpts.provider : activeGeoProvider;
3100
+ const providerName = provider && isString(provider.name) && !isEmpty(provider.name) ? provider.name.trim() : void 0;
3101
+ const rawEndpoint = isString(sourceOrOpts.reverseEndpoint ?? sourceOrOpts.reverseGeoEndpoint) && !isEmpty(sourceOrOpts.reverseEndpoint ?? sourceOrOpts.reverseGeoEndpoint) ? (sourceOrOpts.reverseEndpoint ?? sourceOrOpts.reverseGeoEndpoint).trim() : void 0;
3102
+ const endpoint = rawEndpoint && isValidGeoEndpoint(rawEndpoint) ? rawEndpoint : void 0;
3103
+ if (providerName && endpoint)
3104
+ return `${bucket}:${providerName}:${endpoint}`;
3105
+ if (providerName)
3106
+ return `${bucket}:${providerName}`;
3107
+ if (endpoint)
3108
+ return `${bucket}:${endpoint}`;
3109
+ } else if (activeGeoProvider && isString(activeGeoProvider.name) && !isEmpty(activeGeoProvider.name)) {
3110
+ return `${bucket}:${activeGeoProvider.name.trim()}`;
3111
+ }
3112
+ return bucket;
3113
+ };
3114
+ async function reverseGeocode(coords, opts) {
3115
+ const coerced = coerceGeo(coords);
3116
+ if (!coerced || !isNumber(coerced.latitude) || !isNumber(coerced.longitude))
3117
+ return null;
3118
+ const lat = coerced.latitude;
3119
+ const lng = coerced.longitude;
3120
+ const key = getSpatialCacheKey(lat, lng, opts);
3121
+ const useCache = opts?.refresh !== true;
3122
+ if (useCache) {
3123
+ const cached = reverseGeoCache.get(key);
3124
+ if (cached) {
3125
+ return {
3126
+ ...cached,
3127
+ latitude: lat,
3128
+ longitude: lng,
3129
+ sphere: resolveSphere(cached.sphere, lat),
3130
+ elevation: isNumber(cached.elevation) ? cached.elevation : coerced.elevation,
3131
+ timezone: isString(cached.timezone) ? cached.timezone : coerced.timezone
3132
+ };
3133
+ }
3134
+ }
3135
+ const hasExplicitProvider = isObject(opts) && Object.hasOwn(opts, "provider");
3136
+ const provider = hasExplicitProvider ? opts.provider : activeGeoProvider;
3137
+ if (provider && isFunction(provider.reverseGeocode)) {
3138
+ try {
3139
+ const res = await provider.reverseGeocode(coerced, opts);
3140
+ if (res && isObject(res)) {
3141
+ const result = assembleGeoConfig({ lat, lng }, {
3142
+ sphere: res.sphere,
3143
+ city: res.city,
3144
+ country: res.country,
3145
+ elevation: isNumber(res.elevation) ? res.elevation : coerced.elevation,
3146
+ timezone: isString(res.timezone) ? res.timezone : coerced.timezone
3147
+ });
3148
+ const cachePayload = assembleGeoConfig({ lat, lng }, {
3149
+ sphere: res.sphere,
3150
+ city: res.city,
3151
+ country: res.country,
3152
+ timezone: res.timezone
3153
+ });
3154
+ reverseGeoCache.set(key, cachePayload);
3155
+ return result;
3156
+ }
3157
+ if (opts?.fallback === false)
3158
+ return null;
3159
+ } catch (err) {
3160
+ if (opts?.fallback === false)
3161
+ throw err;
3162
+ }
3163
+ } else if (opts?.fallback === false && (provider || hasExplicitProvider)) {
3164
+ return null;
3165
+ }
3166
+ const rawReverseEndpoint = opts?.reverseEndpoint ?? opts?.reverseGeoEndpoint;
3167
+ if (isText(rawReverseEndpoint)) {
3168
+ const rawEndpoint = rawReverseEndpoint.trim();
3169
+ if (!isValidGeoEndpoint(rawEndpoint))
3170
+ return opts?.fallback === false ? null : coerced;
3171
+ }
3172
+ const customEndpoint = isText(rawReverseEndpoint) ? rawReverseEndpoint.trim() : void 0;
3173
+ const fallbackKey = getSpatialCacheKey(lat, lng, customEndpoint ?? "default");
3174
+ if (useCache) {
3175
+ const cachedFallback = reverseGeoCache.get(fallbackKey);
3176
+ if (cachedFallback) {
3177
+ return {
3178
+ ...cachedFallback,
3179
+ latitude: lat,
3180
+ longitude: lng,
3181
+ sphere: resolveSphere(cachedFallback.sphere, lat),
3182
+ elevation: isNumber(cachedFallback.elevation) ? cachedFallback.elevation : coerced.elevation,
3183
+ timezone: isString(cachedFallback.timezone) ? cachedFallback.timezone : coerced.timezone
3184
+ };
3185
+ }
3186
+ }
3187
+ let endpoint;
3188
+ if (customEndpoint) {
3189
+ if (/\{(?:latitude|lat|longitude|lng|lon)\}/i.test(customEndpoint)) {
3190
+ endpoint = customEndpoint.replace(/\{latitude\}/gi, encodeURIComponent(lat)).replace(/\{lat\}/gi, encodeURIComponent(lat)).replace(/\{longitude\}/gi, encodeURIComponent(lng)).replace(/\{lng\}/gi, encodeURIComponent(lng)).replace(/\{lon\}/gi, encodeURIComponent(lng));
3191
+ } else {
3192
+ const sep = customEndpoint.includes("?") ? "&" : "?";
3193
+ endpoint = `${customEndpoint}${sep}latitude=${encodeURIComponent(lat)}&longitude=${encodeURIComponent(lng)}`;
3194
+ }
3195
+ } else {
3196
+ endpoint = `${DEFAULT_REVERSE_GEO_ENDPOINT}?latitude=${lat}&longitude=${lng}&localityLanguage=en`;
3197
+ }
3198
+ try {
3199
+ const timeout = isNumber(opts?.timeout) && opts.timeout > 0 ? opts.timeout : DEFAULT_REVERSE_GEO_TIMEOUT;
3200
+ const maxBytes = isNumber(opts?.maxBytes) && opts.maxBytes > 0 ? opts.maxBytes : DEFAULT_REVERSE_GEO_MAX_BYTES;
3201
+ const { fetchRequest: fetchRequest2 } = await getRequestModule2();
3202
+ const data = await fetchRequest2(endpoint, {}, { timeout, maxBytes });
3203
+ if (isObject(data)) {
3204
+ const city = isText(data.city) ? data.city.trim() : isText(data.locality) ? data.locality.trim() : isText(data.principalSubdivision) ? data.principalSubdivision.trim() : void 0;
3205
+ const country = isText(data.countryCode) ? data.countryCode.trim() : isText(data.countryName) ? data.countryName.trim() : void 0;
3206
+ const result = assembleGeoConfig({ lat, lng }, {
3207
+ city,
3208
+ country,
3209
+ elevation: coerced.elevation,
3210
+ timezone: coerced.timezone
3211
+ });
3212
+ if (isText(city) || isText(country)) {
3213
+ const cachePayload = assembleGeoConfig({ lat, lng }, {
3214
+ city,
3215
+ country
3216
+ });
3217
+ reverseGeoCache.set(fallbackKey, cachePayload);
3218
+ }
3219
+ return result;
3220
+ }
3221
+ } catch {
3222
+ }
3223
+ return coerced;
3224
+ }
3225
+ async function forwardGeocode(query, opts) {
3226
+ const provider = opts?.provider ?? activeGeoProvider;
3227
+ return provider && isFunction(provider.forwardGeocode) ? provider.forwardGeocode(query, opts) : null;
3228
+ }
3026
3229
  var resolveGeoCoordinates = async (input, opts = {}) => {
3027
3230
  const coerced = coerceGeo(input);
3028
- if (coerced && isNumber(coerced.latitude) && isNumber(coerced.longitude))
3231
+ if (coerced && isNumber(coerced.latitude) && isNumber(coerced.longitude)) {
3232
+ const shouldReverse = opts.reverse === true || opts.autoReverse === true;
3233
+ if (shouldReverse && (!coerced.city || !coerced.country)) {
3234
+ const reversed = await reverseGeocode(coerced, opts);
3235
+ if (reversed) {
3236
+ return {
3237
+ ...reversed,
3238
+ ...coerced,
3239
+ city: isText(coerced.city) ? coerced.city : reversed.city,
3240
+ country: isText(coerced.country) ? coerced.country : reversed.country,
3241
+ lat: coerced.latitude,
3242
+ lng: coerced.longitude,
3243
+ latitude: coerced.latitude,
3244
+ longitude: coerced.longitude
3245
+ };
3246
+ }
3247
+ }
3029
3248
  return {
3030
3249
  ...coerced,
3031
3250
  lat: coerced.latitude,
3032
3251
  lng: coerced.longitude
3033
3252
  };
3253
+ }
3034
3254
  const stashed = opts.refresh === true ? void 0 : getStashedGeo(opts);
3035
3255
  if (stashed && isNumber(stashed.latitude) && isNumber(stashed.longitude))
3036
3256
  return {
@@ -3053,102 +3273,37 @@ var resolveGeoCoordinates = async (input, opts = {}) => {
3053
3273
  }
3054
3274
  return null;
3055
3275
  };
3056
- function haversineDistance(from, to, unit = "km") {
3057
- const c1 = coerceGeo(from);
3058
- const c2 = coerceGeo(to);
3059
- if (!c1 || !c2 || !isNumber(c1.latitude) || !isNumber(c1.longitude) || !isNumber(c2.latitude) || !isNumber(c2.longitude)) {
3060
- return NaN;
3061
- }
3062
- const toRad = Math.PI / 180;
3063
- const lat1 = c1.latitude * toRad;
3064
- const lng1 = c1.longitude * toRad;
3065
- const lat2 = c2.latitude * toRad;
3066
- const lng2 = c2.longitude * toRad;
3067
- const dLat = lat2 - lat1;
3068
- const dLng = lng2 - lng1;
3069
- const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) ** 2;
3070
- const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
3071
- let radius;
3072
- if (unit === "miles") {
3073
- radius = 3958.7613;
3074
- } else if (unit === "m") {
3075
- radius = 63710088e-1;
3076
- } else {
3077
- radius = 6371.0088;
3078
- }
3079
- const dist = radius * c;
3080
- return unit === "m" ? Math.round(dist) : Math.round(dist * 1e3) / 1e3;
3081
- }
3082
- var resolveCivilTimezoneOffset = (tz, epochMs) => {
3083
- try {
3084
- if (typeof Temporal !== "undefined" && typeof Temporal.Instant?.fromEpochMilliseconds === "function") {
3085
- const zdt = Temporal.Instant.fromEpochMilliseconds(epochMs).toZonedDateTimeISO(tz);
3086
- return zdt.offsetNanoseconds / 6e10;
3087
- }
3088
- const parts = new Intl.DateTimeFormat("en-US", {
3089
- timeZone: tz,
3090
- timeZoneName: "longOffset",
3091
- year: "numeric"
3092
- }).formatToParts(new Date(epochMs));
3093
- const tzPart = parts.find((p) => p.type === "timeZoneName")?.value;
3094
- if (tzPart) {
3095
- if (tzPart === "GMT" || tzPart === "UTC")
3096
- return 0;
3097
- const match = tzPart.match(/GMT([+-])(\d{2}):(\d{2})/);
3098
- if (match) {
3099
- const sign = match[1] === "-" ? -1 : 1;
3100
- return sign * (parseInt(match[2], 10) * 60 + parseInt(match[3], 10));
3101
- }
3102
- }
3103
- } catch {
3104
- }
3105
- return void 0;
3106
- };
3107
- function solarOffset(coords, options) {
3108
- const geo = coerceGeo(coords);
3109
- if (!geo || !isNumber(geo.latitude) || !isNumber(geo.longitude) || isNaN(geo.latitude) || isNaN(geo.longitude)) {
3110
- return NaN;
3111
- }
3112
- const lng = geo.longitude;
3113
- const tz = options?.timeZone ?? (isReference(coords) ? coords.timezone ?? coords.tz : void 0) ?? geo.timezone;
3114
- const dateVal = options?.date ?? (isReference(coords) && isNumber(coords.epoch?.ms) ? coords.epoch.ms : Date.now());
3115
- const epochMs = typeof dateVal === "number" ? dateVal : isString(dateVal) ? Date.parse(dateVal) : isDate(dateVal) ? dateVal.getTime() : isReference(dateVal) && isNumber(dateVal?.epoch?.ms) ? dateVal.epoch.ms : Date.now();
3116
- if (!Number.isFinite(epochMs))
3117
- return NaN;
3118
- const offsetMinutes = isText(tz) ? resolveCivilTimezoneOffset(tz, epochMs) : void 0;
3119
- const refMeridian = offsetMinutes !== void 0 ? offsetMinutes / 60 * 15 : Math.round(lng / 15) * 15;
3120
- const deltaLng = lng - refMeridian;
3121
- let offsetMin = deltaLng * 4;
3122
- if (options?.apparent) {
3123
- const d = new Date(epochMs);
3124
- const startOfYear = Date.UTC(d.getUTCFullYear(), 0, 1);
3125
- const dayOfYear = Math.floor((epochMs - startOfYear) / 864e5) + 1;
3126
- const gamma = 2 * Math.PI / 365 * (dayOfYear - 1);
3127
- const eqTime = 229.18 * (75e-6 + 1868e-6 * Math.cos(gamma) - 0.032077 * Math.sin(gamma) - 0.014615 * Math.cos(2 * gamma) - 0.040849 * Math.sin(2 * gamma));
3128
- offsetMin += eqTime;
3129
- }
3130
- const unit = options?.unit ?? "minutes";
3131
- let res;
3132
- if (unit === "seconds") {
3133
- res = offsetMin * 60;
3134
- } else if (unit === "hours") {
3135
- res = offsetMin / 60;
3136
- } else {
3137
- res = offsetMin;
3138
- }
3139
- const precision = options?.precision ?? 2;
3140
- const factor = Math.pow(10, precision);
3141
- return Math.round(res * factor) / factor;
3142
- }
3143
3276
 
3144
3277
  // src/index.ts
3145
3278
  init_mapper_library2();
3146
3279
  init_mapper_library();
3147
3280
  init_assertion_library();
3281
+ import {
3282
+ resolveCulturalLocale
3283
+ } from "@magmacomputing/tempo-fns";
3284
+ var NON_GEO_CONTROL_KEYS = [
3285
+ "setTimezone",
3286
+ "setLocale",
3287
+ "refresh",
3288
+ "ttl",
3289
+ "endpoint",
3290
+ "timeout",
3291
+ "maxBytes",
3292
+ "highAccuracy",
3293
+ "catch",
3294
+ "debug",
3295
+ "geo",
3296
+ "provider",
3297
+ "fallback",
3298
+ "reverse",
3299
+ "autoReverse"
3300
+ ];
3148
3301
  var GeoPlugin = definePlugin({
3149
3302
  name: "geo",
3150
3303
  install(TempoClass, options) {
3151
3304
  const installedClass = TempoClass || this;
3305
+ if (options?.provider)
3306
+ setGeoProvider(options.provider);
3152
3307
  const getEffectiveOptions = (callSiteOpts, instance) => {
3153
3308
  const classOpts = installedClass.config?.pluginOptions?.geo;
3154
3309
  const instanceOpts = instance?.config?.pluginOptions?.geo;
@@ -3164,11 +3319,13 @@ var GeoPlugin = definePlugin({
3164
3319
  lookup: (opts) => geoLookup(getEffectiveOptions(opts)),
3165
3320
  resolve: (target, opts) => resolveGeoCoordinates(target, getEffectiveOptions(opts, target)),
3166
3321
  coerce: coerceGeo,
3167
- distance: haversineDistance,
3168
- solarOffset,
3169
3322
  stash: stashGeo,
3170
3323
  clear: clearStashedGeo,
3171
3324
  get: getStashedGeo,
3325
+ setProvider: setGeoProvider,
3326
+ getProvider: getGeoProvider,
3327
+ reverse: (coords, opts) => reverseGeocode(coords, getEffectiveOptions(opts)),
3328
+ forward: (query, opts) => forwardGeocode(query, getEffectiveOptions(opts)),
3172
3329
  server: serverGeoLocation,
3173
3330
  browser: geoLocation,
3174
3331
  get current() {
@@ -3185,6 +3342,7 @@ var GeoPlugin = definePlugin({
3185
3342
  TempoClass.prototype.geoLocate = async function(opts) {
3186
3343
  const effectiveOpts = getEffectiveOptions(opts, this);
3187
3344
  const setTimezone = effectiveOpts?.setTimezone !== false;
3345
+ const setLocale = effectiveOpts?.setLocale ?? true;
3188
3346
  const coords = await resolveGeoCoordinates(this, effectiveOpts);
3189
3347
  if (coords) {
3190
3348
  const existingGeo = isObject(this.config.geo) ? this.config.geo : {};
@@ -3196,7 +3354,7 @@ var GeoPlugin = definePlugin({
3196
3354
  const callSiteGeo = coerceGeo(opts) ?? {};
3197
3355
  if (isObject(opts)) {
3198
3356
  for (const key of Object.keys(opts)) {
3199
- if (isSafeKey(key) && !["setTimezone", "refresh", "ttl", "endpoint", "timeout", "catch", "debug", "geo"].includes(key)) {
3357
+ if (isSafeKey(key) && !NON_GEO_CONTROL_KEYS.includes(key)) {
3200
3358
  if (!GEO_PROPERTIES.includes(key))
3201
3359
  customKeys[key] = opts[key];
3202
3360
  }
@@ -3209,17 +3367,20 @@ var GeoPlugin = definePlugin({
3209
3367
  ...isNumber(preservedElevation) ? { elevation: preservedElevation } : {},
3210
3368
  ...callSiteGeo
3211
3369
  };
3212
- if (isNumber(mergedGeo.latitude)) mergedGeo.latitude = Math.round(mergedGeo.latitude * 1e3) / 1e3;
3213
- if (isNumber(mergedGeo.longitude)) mergedGeo.longitude = Math.round(mergedGeo.longitude * 1e3) / 1e3;
3214
- if (!mergedGeo.sphere && isNumber(mergedGeo.latitude)) {
3215
- mergedGeo.sphere = mergedGeo.latitude > 1e-3 ? "north" : mergedGeo.latitude < -1e-3 ? "south" : "equator";
3216
- }
3370
+ const lat = mergedGeo.latitude;
3371
+ const lng = mergedGeo.longitude;
3372
+ if (isNumber(lat)) mergedGeo.latitude = Math.round(lat * 1e3) / 1e3;
3373
+ if (isNumber(lng)) mergedGeo.longitude = Math.round(lng * 1e3) / 1e3;
3374
+ if (!mergedGeo.sphere && isNumber(lat))
3375
+ mergedGeo.sphere = lat > 1e-3 ? "north" : lat < -1e-3 ? "south" : "equator";
3217
3376
  let instance = this;
3218
3377
  const targetTz = mergedGeo.timezone;
3219
3378
  if (setTimezone && isString(targetTz) && !isEmpty(targetTz))
3220
3379
  instance = this.set({ timeZone: targetTz });
3380
+ const targetLocale = mergedGeo.country || isString(setLocale) ? resolveCulturalLocale(this.locale ?? this.config.locale, mergedGeo.country, setLocale) : void 0;
3221
3381
  return new TempoClass(instance, {
3222
3382
  ...instance.config,
3383
+ ...targetLocale ? { locale: targetLocale } : {},
3223
3384
  geo: (0, sdk_exports.deepFreeze)(mergedGeo),
3224
3385
  ...mergedGeo.sphere ? { sphere: mergedGeo.sphere } : {}
3225
3386
  });
@@ -3229,12 +3390,6 @@ var GeoPlugin = definePlugin({
3229
3390
  TempoClass.prototype.geoLookup = async function(opts) {
3230
3391
  return resolveGeoCoordinates(this, getEffectiveOptions(opts, this));
3231
3392
  };
3232
- TempoClass.prototype.geoDistance = function(other, unit) {
3233
- return haversineDistance(this, other, unit);
3234
- };
3235
- TempoClass.prototype.geoSolarOffset = function(options2) {
3236
- return solarOffset(this, options2);
3237
- };
3238
3393
  }
3239
3394
  });
3240
3395
  var geoPlugin = GeoPlugin;
@@ -3245,18 +3400,22 @@ var index_default = GeoPlugin;
3245
3400
  export {
3246
3401
  GEO_PROPERTIES,
3247
3402
  GeoPlugin,
3403
+ NON_GEO_CONTROL_KEYS,
3248
3404
  clearStashedGeo,
3249
3405
  coerceGeo,
3250
3406
  index_default as default,
3407
+ forwardGeocode,
3251
3408
  geoLocation,
3252
3409
  geoLookup,
3253
3410
  geoPlugin,
3411
+ getGeoProvider,
3254
3412
  getStashedGeo,
3255
- haversineDistance,
3413
+ resolveCulturalLocale,
3256
3414
  resolveGeoCoordinates,
3415
+ reverseGeocode,
3257
3416
  serverGeoCoords,
3258
3417
  serverGeoLocation,
3259
3418
  serverMapHemisphere,
3260
- solarOffset,
3419
+ setGeoProvider,
3261
3420
  stashGeo
3262
3421
  };