diff --git a/README.md b/README.md index fb9fcb0..54d3676 100644 --- a/README.md +++ b/README.md @@ -55,11 +55,13 @@ The `inputData` object can contain the following properties, used to generate ge - `lat` (number): Latitude coordinate. No default value. - `lon` (number): Longitude coordinate. No default value. +- `geohash` (string): Geohash coordinate. No default value. Will be ignored if lat & lon are also passed as input as they take precedence. - `city` (string): Name of the city. No default value. - `country` (string): Name of the country. No default value. - `regionName` (string): Name of the region or state. No default value. - `countryCode` (string): `ISO-3166-1:alpha-2` country code. No default value. - Other properties (any): Additionally, key value pairs will be ignored but do not throw an error. +- `planetName` (string): Name of a planet. ## Options Reference The `options` object specifies which types of tags to generate. @@ -77,10 +79,16 @@ The `options` object specifies which types of tags to generate. Please note: that these will only have an effect on the output if the input for their corresponding values were set. This is especially true for passthrough values. Some of these passthrough values may be deduped if they are not unique against ISO values. - `geohash` (boolean): Includes geohash codes from `ngeohash`, with diminishing resolution, based on latitude and longitude. Default: `true`. -- `city` (boolean): Include a tag for the city. Default: `true`. -- `country` (boolean): Include a tag for the country. Default: `true`. -- `region` (boolean): Include a tag for the region. Default:`true`. +- `city` or `cityName` (boolean): Include a tag for the city in response **if available**. Default: `true`. +- `country` (boolean): Include a tag for the `countryCode` and `countryName` in response **if available**. Default: `true`. +- `countryCode` (boolean): Include a tag for the `countryCode` in response **if available**. Default: `true`. +- `countryName` (boolean): Include a tag for the `countryName` in response **if available**. Default: `true`. +- `region` (boolean): Include a tag for the `regionCode` and `regionName` in response **if available**. Default:`true`. +- `regionCode` (boolean): Include a tag for the `regionCode` in response **if available**. Default:`true`. +- `regionName` (boolean): Include a tag for the `regionName` in response **if available**. Default:`true`. - `gps` (boolean): Include latitude and longitude as a 'dd' tag (de-factor GPS standards) and separate tags for lat and lon with diminishing resolution. Default: `false`. +- `planet` or `planetName` (boolean): Include a tag for the `planetName` in response **if available**. Default: `false`. + ## Response Reference @@ -104,11 +112,11 @@ Which tags you use depend on use-case. If your concerns are namely geospacial, u 3. **ISO-3166-1 Codes**: - These tags represent country information derived from the `iso-3166` library and are based on the provided `countryCode` input value. They are not passthrough. - Examples - - (`isoAsNamespace==false [default]`) + - **`isoAsNamespace==false [default]`** - Alpha-2 code: `[ 'g', 'HU', 'countryCode' ]` - Alpha-3 code: `[ 'g', 'HUN', 'countryCode']` - Numeric code: `[ 'g', '348', 'countryCode' ]` - - (`isoAsNamespace==true`) + - **`isoAsNamespace==true`** - Alpha-2 code: `[ 'g', 'HU', 'ISO-3166-1' ]` - Alpha-3 code: `[ 'g', 'HUN', 'ISO-3166-1']` - Numeric code: `[ 'g', '348', 'ISO-3166-1' ]` diff --git a/package.json b/package.json index bf78bcf..0189c00 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nostr-geotags", - "version": "0.3.1", + "version": "0.4.0", "description": "Give an object of geodata, returns standardized nostr geotags ", "type": "module", "main": "dist/index.js", diff --git a/src/index.test.ts b/src/index.test.ts index cec0ba8..daa007c 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi} from 'vitest'; -import ngeotags, { InputData, Option, filterNonStringTags, generateCountryTagKey, sortTagsByKey, GeoTags, filterOutType, iso31661Namespace, iso31662Namespace, iso31663Namespace } from './index'; // Adjust the import path as needed +import ngeotags, { calculateResolution, InputData, Option, filterNonStringTags, generateCountryTagKey, sortTagsByKey, GeoTags, filterOutType, iso31661Namespace, iso31662Namespace, iso31663Namespace } from './index'; // Adjust the import path as needed describe('generateTags()', () => { @@ -298,7 +298,7 @@ describe('generateTags()', () => { ])); }); - const maxResolution = 10; // This should match the value used in your function + const maxResolution = 9; // This should match the value used in your function it('handles maximum decimal length', () => { const input = { lat: 47.12345678901, lon: 19.12345678901 }; @@ -386,7 +386,6 @@ describe('generateTags()', () => { it('should handle geohash correctly', () => { const input: InputData = { - geohash: true, lat: 47.5636, lon: 19.0947 }; @@ -395,6 +394,41 @@ describe('generateTags()', () => { expect(result.some(tag => tag[0] === 'g')).toBeTruthy(); }); + + it('should decode geohash when geohash passed via input, either lat or lon are null, and gps is enabled', () => { + const input: InputData = { + geohash: 'u2mwdd8q4' + }; + + const result = ngeotags(input, { gps: true }); + console.log('hash', result) + expect(result.some(tag => tag[0] === 'g')).toBeTruthy(); + }); + + it('should inherit default resolution when one is not set', () => { + var result = calculateResolution(12.3456789876545345455, undefined) + expect(result).toBe(9) + }) + + it('should ignore geohash when geohash passed via input, both lat and long are set (number) and gps is enabled', () => { + const input: InputData = { + geohash: 'h9xhn7y', + lat: 47.56361246109009, + lon: 19.094688892364502 + }; + + const result = ngeotags(input, { gps: true }); + console.log('hash and dd passed', result) + expect(result.some(tag => tag[0] === 'g')).toBeTruthy(); + expect(result).toEqual(expect.arrayContaining([ + [ 'g', '47.5636', 'lat' ], + [ 'g', '19.0946', 'lon' ], + ])); + }); + + + + it('should handle ISO-3166-1 correctly with optimistic input', () => { const input: InputData = { iso31661: true, diff --git a/src/index.ts b/src/index.ts index 7cbd6a0..3461929 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,8 @@ -import ngeohash from 'ngeohash'; +import ngeohash, { GeographicPoint } from 'ngeohash'; import { iso31661, iso31662, iso31663, ISO31661AssignedEntry, ISO31662Entry, ISO31661Entry } from 'iso-3166'; export interface InputData { + geohash?: string, lat?: number; lon?: number; cityName?: string; @@ -13,12 +14,13 @@ export interface InputData { } export interface Options { - dedupe?: boolean; sort?: boolean; isoAsNamespace?: boolean; unM49AsNamespace?: boolean; + ddMaxResolution?: number; + iso31661?: boolean, iso31662?: boolean, iso31663?: boolean, @@ -99,6 +101,8 @@ export type LabelTag = LabelNamespace | Label; */ export type GeoTags = Geohash | LabelTag; +const DD_MAX_RES_DEFAULT = 9 + /** * Retrieves updated ISO-3166-3 values based on a given code. * @@ -131,6 +135,26 @@ export const iso31662Namespace = (opts: Options): string => opts.isoAsNamespace export const iso31663Namespace = (opts: Options): string => opts.isoAsNamespace ? 'ISO-3166-3' : 'countryCode'; +/** + * Truncates a number (float) to a specified precision. Generally used for dd (lat and lon) values. + * + * @param {number} num - The float to be shortened. + * @param {number} resolution - How many decimal places. + * @returns {GeoTags[]} An array of generated geo tags. + * + * This function shortens a lat or lon to a specified precision (number of decimal places.) + * Does nothing if whole number. + */ +const truncateToResolution = (num: number, resolution: number): number => { + const multiplier = Math.pow(10, resolution); + return Math.floor(num * multiplier) / multiplier; +}; + + +export const calculateResolution = (input: number, max: number | undefined): number => { + if(!max) max = DD_MAX_RES_DEFAULT + return input % 1 === 0 ? 1 : Math.min(input.toString().split('.')?.[1]?.length, max); +} /** * Generates an array of `g` tags based on the input data and options provided. @@ -141,24 +165,24 @@ export const iso31663Namespace = (opts: Options): string => opts.isoAsNamespace * * This function processes the input data and generates a series of tags based on the options. * It handles various types of data such as GPS coordinates, ISO-3166 country and region codes, - * city. The generated tags are deduplicated by default, can be changed - * with dedupe option. + * city. */ const generateTags = (input: InputData, opts: Options): GeoTags[] => { const tags: GeoTags[] = []; // GPS - if (opts.gps && input.lat && input.lon) { + if(opts?.gps && input.geohash && (!input?.lat || !input?.lon)) { + const dd = ngeohash.decode(input.geohash) + console.log('wtf', dd) + input.lat = dd.latitude + input.lon = dd.longitude + } + if (opts?.gps && input.lat && input.lon) { tags.push(['G', `dd`]); tags.push(['g', `${input.lat}, ${input.lon}`, 'dd']); - - const maxResolution = 10; - const truncateToResolution = (num: number, resolution: number): number => { - const multiplier = Math.pow(10, resolution); - return Math.floor(num * multiplier) / multiplier; - }; - const latResolution = input.lat % 1 === 0 ? 1 : Math.min(input.lat.toString().split('.')[1].length, maxResolution); - const lonResolution = input.lon % 1 === 0 ? 1 : Math.min(input.lon.toString().split('.')[1].length, maxResolution); + + const latResolution = calculateResolution(input.lat, opts.ddMaxResolution); + const lonResolution = calculateResolution(input.lon, opts.ddMaxResolution); tags.push(['G', `lat`]); for (let i = latResolution; i > 0; i--) { @@ -263,7 +287,6 @@ const generateTags = (input: InputData, opts: Options): GeoTags[] => { const namespace = iso31662Namespace(opts) result = filterOutType(result, namespace); } - // result = opts?.dedupe === true? dedupe(result): result; result = opts?.sort === true? sortTagsByKey(result): result; result = sanitize(result) return result @@ -365,12 +388,13 @@ export default (input: InputData | null, opts?: Options): GeoTags[] => { if (!(input instanceof Object) || Array.isArray(input) || typeof input!== 'object' || typeof input=== 'function' ) throw new Error('Input must be an object'); opts = { - dedupe: true, sort: false, isoAsNamespace: false, unM49AsNamespace: true, + ddMaxResolution: DD_MAX_RES_DEFAULT, + iso31661: true, iso31662: false, iso31663: false,