export const GEOHASH_PRECISION = 10 ; export const BASE32 = '0123456789bcdefghjkmnpqrstuvwxyz' ; export const EARTH_MERI_CIRCUMFERENCE = 40007860 ; export const METERS_PER_DEGREE_LATITUDE = 110574 ; export const BITS_PER_CHAR = 5 ; export const MAXIMUM_BITS_PRECISION = 22 * BITS_PER_CHAR ; export const EARTH_EQ_RADIUS = 6378137.0 ; export const E2 = 0.00669447819799 ; export const EPSILON = 1e-12 ; export type Geopoint = [ number , number ] ; export type Geohash = string ; export type GeohashRange = [ Geohash , Geohash ] ; function log2 ( x ) { return Math . log ( x ) / Math . log ( 2 ) ; } param key export function validateKey ( key ) { let error ; if ( typeof key !== 'string' ) { error = 'key must be a string' ; } else if ( key . length === 0 ) { error = 'key cannot be the empty string' ; } else if ( 1 + GEOHASH_PRECISION + key . length > 755 ) { error = 'key is too long to be stored in Firebase' ; } else if ( /[\[\].#$\/\u0000-\u001F\u007F]/ . test ( key ) ) { error = 'key cannot contain any of the following characters: . # $ ] [ /' ; } if ( typeof error !== 'undefined' ) { throw new Error ( 'Invalid GeoFire key \'' + key + '\': ' + error ) ; } } param location export function validateLocation ( location ) { let error ; if ( ! Array . isArray ( location ) ) { error = 'location must be an array' ; } else if ( location . length !== 2 ) { error = 'expected array of length 2, got length ' + location . length ; } else { const latitude = location [ 0 ] ; const longitude = location [ 1 ] ; if ( typeof latitude !== 'number' || isNaN ( latitude ) ) { error = 'latitude must be a number' ; } else if ( latitude < - 90 || latitude > 90 ) { error = 'latitude must be within the range [-90, 90]' ; } else if ( typeof longitude !== 'number' || isNaN ( longitude ) ) { error = 'longitude must be a number' ; } else if ( longitude < - 180 || longitude > 180 ) { error = 'longitude must be within the range [-180, 180]' ; } } if ( typeof error !== 'undefined' ) { throw new Error ( 'Invalid GeoFire location \'' + location + '\': ' + error ) ; } } param geohash export function validateGeohash ( geohash ) { let error ; if ( typeof geohash !== 'string' ) { error = 'geohash must be a string' ; } else if ( geohash . length === 0 ) { error = 'geohash cannot be the empty string' ; } else { for ( const letter of geohash ) { if ( BASE32 . indexOf ( letter ) === - 1 ) { error = 'geohash cannot contain \'' + letter + '\'' ; } } } if ( typeof error !== 'undefined' ) { throw new Error ( 'Invalid GeoFire geohash \'' + geohash + '\': ' + error ) ; } } param degrees returns export function degreesToRadians ( degrees ) { if ( typeof degrees !== 'number' || isNaN ( degrees ) ) { throw new Error ( 'Error: degrees must be a number' ) ; } return ( degrees * Math . PI / 180 ) ; } param location param precision returns export function geohashForLocation ( location , precision = GEOHASH_PRECISION ) { validateLocation ( location ) ; if ( typeof precision !== 'undefined' ) { if ( typeof precision !== 'number' || isNaN ( precision ) ) { throw new Error ( 'precision must be a number' ) ; } else if ( precision <= 0 ) { throw new Error ( 'precision must be greater than 0' ) ; } else if ( precision > 22 ) { throw new Error ( 'precision cannot be greater than 22' ) ; } else if ( Math . round ( precision ) !== precision ) { throw new Error ( 'precision must be an integer' ) ; } } const latitudeRange = { min : - 90 , max : 90 } ; const longitudeRange = { min : - 180 , max : 180 } ; let hash = '' ; let hashVal = 0 ; let bits = 0 ; let even = 1 ; while ( hash . length < precision ) { const val = even ? location [ 1 ] : location [ 0 ] ; const range = even ? longitudeRange : latitudeRange ; const mid = ( range . min + range . max ) / 2 ; if ( val > mid ) { hashVal = ( hashVal << 1 ) + 1 ; range . min = mid ; } else { hashVal = ( hashVal << 1 ) + 0 ; range . max = mid ; } even = ! even ; if ( bits < 4 ) { bits ++ ; } else { bits = 0 ; hash += BASE32 [ hashVal ] ; hashVal = 0 ; } } return hash ; } param distance param latitude returns export function metersToLongitudeDegrees ( distance , latitude ) { const radians = degreesToRadians ( latitude ) ; const num = Math . cos ( radians ) * EARTH_EQ_RADIUS * Math . PI / 180 ; const denom = 1 / Math . sqrt ( 1 - E2 * Math . sin ( radians ) * Math . sin ( radians ) ) ; const deltaDeg = num * denom ; if ( deltaDeg < EPSILON ) { return distance > 0 ? 360 : 0 ; } else { return Math . min ( 360 , distance / deltaDeg ) ; } } param resolution param latitude return export function longitudeBitsForResolution ( resolution , latitude ) { const degs = metersToLongitudeDegrees ( resolution , latitude ) ; return ( Math . abs ( degs ) > 0.000001 ) ? Math . max ( 1 , log2 ( 360 / degs ) ) : 1 ; } param resolution returns export function latitudeBitsForResolution ( resolution ) { return Math . min ( log2 ( EARTH_MERI_CIRCUMFERENCE / 2 / resolution ) , MAXIMUM_BITS_PRECISION ) ; } param longitude returns export function wrapLongitude ( longitude ) { if ( longitude <= 180 && longitude >= - 180 ) { return longitude ; } const adjusted = longitude + 180 ; if ( adjusted > 0 ) { return ( adjusted % 360 ) - 180 ; } else { return 180 - ( - adjusted % 360 ) ; } } param coordinate param size returns export function boundingBoxBits ( coordinate , size ) { const latDeltaDegrees = size / METERS_PER_DEGREE_LATITUDE ; const latitudeNorth = Math . min ( 90 , coordinate [ 0 ] + latDeltaDegrees ) ; const latitudeSouth = Math . max ( - 90 , coordinate [ 0 ] - latDeltaDegrees ) ; const bitsLat = Math . floor ( latitudeBitsForResolution ( size ) ) * 2 ; const bitsLongNorth = Math . floor ( longitudeBitsForResolution ( size , latitudeNorth ) ) * 2 - 1 ; const bitsLongSouth = Math . floor ( longitudeBitsForResolution ( size , latitudeSouth ) ) * 2 - 1 ; return Math . min ( bitsLat , bitsLongNorth , bitsLongSouth , MAXIMUM_BITS_PRECISION ) ; } param center param radius returns export function boundingBoxCoordinates ( center , radius ) { const latDegrees = radius / METERS_PER_DEGREE_LATITUDE ; const latitudeNorth = Math . min ( 90 , center [ 0 ] + latDegrees ) ; const latitudeSouth = Math . max ( - 90 , center [ 0 ] - latDegrees ) ; const longDegsNorth = metersToLongitudeDegrees ( radius , latitudeNorth ) ; const longDegsSouth = metersToLongitudeDegrees ( radius , latitudeSouth ) ; const longDegs = Math . max ( longDegsNorth , longDegsSouth ) ; return [ [ center [ 0 ] , center [ 1 ] ] , [ center [ 0 ] , wrapLongitude ( center [ 1 ] - longDegs ) ] , [ center [ 0 ] , wrapLongitude ( center [ 1 ] + longDegs ) ] , [ latitudeNorth , center [ 1 ] ] , [ latitudeNorth , wrapLongitude ( center [ 1 ] - longDegs ) ] , [ latitudeNorth , wrapLongitude ( center [ 1 ] + longDegs ) ] , [ latitudeSouth , center [ 1 ] ] , [ latitudeSouth , wrapLongitude ( center [ 1 ] - longDegs ) ] , [ latitudeSouth , wrapLongitude ( center [ 1 ] + longDegs ) ] ] ; } param geohash param bits returns export function geohashQuery ( geohash , bits ) { validateGeohash ( geohash ) ; const precision = Math . ceil ( bits / BITS_PER_CHAR ) ; if ( geohash . length < precision ) { return [ geohash , geohash + '~' ] ; } geohash = geohash . substring ( 0 , precision ) ; const base = geohash . substring ( 0 , geohash . length - 1 ) ; const lastValue = BASE32 . indexOf ( geohash . charAt ( geohash . length - 1 ) ) ; const significantBits = bits - ( base . length * BITS_PER_CHAR ) ; const unusedBits = ( BITS_PER_CHAR - significantBits ) ; const startValue = ( lastValue >> unusedBits ) << unusedBits ; const endValue = startValue + ( 1 << unusedBits ) ; if ( endValue > 31 ) { return [ base + BASE32 [ startValue ] , base + '~' ] ; } else { return [ base + BASE32 [ startValue ] , base + BASE32 [ endValue ] ] ; } } param center param radius return export function geohashQueryBounds ( center , radius ) { validateLocation ( center ) ; const queryBits = Math . max ( 1 , boundingBoxBits ( center , radius ) ) ; const geohashPrecision = Math . ceil ( queryBits / BITS_PER_CHAR ) ; const coordinates = boundingBoxCoordinates ( center , radius ) ; const queries = coordinates . map ( ( coordinate ) => { return geohashQuery ( geohashForLocation ( coordinate , geohashPrecision ) , queryBits ) ; } ) ; return queries . filter ( ( query , index ) => { return ! queries . some ( ( other , otherIndex ) => { return index > otherIndex && query [ 0 ] === other [ 0 ] && query [ 1 ] === other [ 1 ] ; } ) ; } ) ; } param location1 param location2 returns export function distanceBetween ( location1 , location2 ) { validateLocation ( location1 ) ; validateLocation ( location2 ) ; const radius = 6371 ; const latDelta = degreesToRadians ( location2 [ 0 ] - location1 [ 0 ] ) ; const lonDelta = degreesToRadians ( location2 [ 1 ] - location1 [ 1 ] ) ; const a = ( Math . sin ( latDelta / 2 ) * Math . sin ( latDelta / 2 ) ) + ( Math . cos ( degreesToRadians ( location1 [ 0 ] ) ) * Math . cos ( degreesToRadians ( location2 [ 0 ] ) ) * Math . sin ( lonDelta / 2 ) * Math . sin ( lonDelta / 2 ) ) ; const c = 2 * Math . atan2 ( Math . sqrt ( a ) , Math . sqrt ( 1 - a ) ) ; return radius * c ; }