geohash各种计算方法

2023-01-15  本文已影响0人  很好就这样吧

Geohash算法介绍

参考文章:https://blog.csdn.net/usher_ou/article/details/122716877
GeoHash是空间索引的一种方式,其基本原理是将地球理解为一个二维平面,通过把二维的空间经纬度数据编码为一个字符串,可以把平面递归分解成更小的子块,每个子块在一定经纬度范围内拥有相同的编码。

以GeoHash方式建立空间索引,可以提高对空间poi数据进行经纬度检索的效率。

编码规则为:先将纬度范围(-90, 90)平分成两个区间(-90, 0)和(0, 90),如果目标维度位于前一个区间,则编码为0,否则编码为1,然后根据目标纬度所落的区间再平均分成两个区间进行编码,以此类推,直到精度满足要求,经度也用同样的算法,对(-180, 180)依次细分,然后合并经度和纬度的编码,奇数位放纬度,偶数位放经度,组成一串新的二进制编码,按照Base32进行编码。

算法库

参考文章:https://blog.csdn.net/qq_41737172/article/details/118539350

var BASE32_CODES = "0123456789bcdefghjkmnpqrstuvwxyz"
var BASE32_CODES_DICT = {}
for (var i = 0; i < BASE32_CODES.length; i++) {
  BASE32_CODES_DICT[BASE32_CODES.charAt(i)] = i
}

var ENCODE_AUTO = "auto"

var MIN_LAT = -90
var MAX_LAT = 90
var MIN_LON = -180
var MAX_LON = 180
/**
 * Significant Figure Hash Length
 *
 * This is a quick and dirty lookup to figure out how long our hash
 * should be in order to guarantee a certain amount of trailing
 * significant figures. This was calculated by determining the error:
 * 45/2^(n-1) where n is the number of bits for a latitude or
 * longitude. Key is # of desired sig figs, value is minimum length of
 * the
 * @type Array
 */
//     Desired sig figs:  0  1  2  3  4   5   6   7   8   9  10
var SIGFIG_HASH_LENGTH = [0, 5, 7, 8, 11, 12, 13, 15, 16, 17, 18]
/**
 * Encode
 *
 * Create a Geohash out of a latitude and longitude that is
 * `numberOfChars` long.
 *
 * @param {Number|String} latitude
 * @param {Number|String} longitude
 * @param {Number} numberOfChars
 * @returns {String}
 */
var encode = function (latitude, longitude, numberOfChars) {
  if (numberOfChars === ENCODE_AUTO) {
    if (typeof latitude === "number" || typeof longitude === "number") {
      throw new Error("string notation required for auto precision.")
    }
    var decSigFigsLat = latitude.split(".")[1].length
    var decSigFigsLong = longitude.split(".")[1].length
    var numberOfSigFigs = Math.max(decSigFigsLat, decSigFigsLong)
    numberOfChars = SIGFIG_HASH_LENGTH[numberOfSigFigs]
  } else if (numberOfChars === undefined) {
    numberOfChars = 9
  }

  var chars = [],
    bits = 0,
    bitsTotal = 0,
    hash_value = 0,
    maxLat = MAX_LAT,
    minLat = MIN_LAT,
    maxLon = MAX_LON,
    minLon = MIN_LON,
    mid
  while (chars.length < numberOfChars) {
    if (bitsTotal % 2 === 0) {
      mid = (maxLon + minLon) / 2
      if (longitude > mid) {
        hash_value = (hash_value << 1) + 1
        minLon = mid
      } else {
        hash_value = (hash_value << 1) + 0
        maxLon = mid
      }
    } else {
      mid = (maxLat + minLat) / 2
      if (latitude > mid) {
        hash_value = (hash_value << 1) + 1
        minLat = mid
      } else {
        hash_value = (hash_value << 1) + 0
        maxLat = mid
      }
    }

    bits++
    bitsTotal++
    if (bits === 5) {
      var code = BASE32_CODES[hash_value]
      chars.push(code)
      bits = 0
      hash_value = 0
    }
  }
  return chars.join("")
}

/**
 * Encode Integer
 *
 * Create a Geohash out of a latitude and longitude that is of "bitDepth".
 *
 * @param {Number} latitude
 * @param {Number} longitude
 * @param {Number} bitDepth
 * @returns {Number}
 */
var encode_int = function (latitude, longitude, bitDepth) {
  bitDepth = bitDepth || 52

  var bitsTotal = 0,
    maxLat = MAX_LAT,
    minLat = MIN_LAT,
    maxLon = MAX_LON,
    minLon = MIN_LON,
    mid,
    combinedBits = 0

  while (bitsTotal < bitDepth) {
    combinedBits *= 2
    if (bitsTotal % 2 === 0) {
      mid = (maxLon + minLon) / 2
      if (longitude > mid) {
        combinedBits += 1
        minLon = mid
      } else {
        maxLon = mid
      }
    } else {
      mid = (maxLat + minLat) / 2
      if (latitude > mid) {
        combinedBits += 1
        minLat = mid
      } else {
        maxLat = mid
      }
    }
    bitsTotal++
  }
  return combinedBits
}

/**
 * Decode Bounding Box
 *
 * Decode hashString into a bound box matches it. Data returned in a four-element array: [minlat, minlon, maxlat, maxlon]
 * @param {String} hash_string
 * @returns {Array}
 */
var decode_bbox = function (hash_string) {
  var isLon = true,
    maxLat = MAX_LAT,
    minLat = MIN_LAT,
    maxLon = MAX_LON,
    minLon = MIN_LON,
    mid

  var hashValue = 0
  for (var i = 0, l = hash_string.length; i < l; i++) {
    var code = hash_string[i].toLowerCase()
    hashValue = BASE32_CODES_DICT[code]

    for (var bits = 4; bits >= 0; bits--) {
      var bit = (hashValue >> bits) & 1
      if (isLon) {
        mid = (maxLon + minLon) / 2
        if (bit === 1) {
          minLon = mid
        } else {
          maxLon = mid
        }
      } else {
        mid = (maxLat + minLat) / 2
        if (bit === 1) {
          minLat = mid
        } else {
          maxLat = mid
        }
      }
      isLon = !isLon
    }
  }
  return [minLat, minLon, maxLat, maxLon]
}

/**
 * Decode Bounding Box Integer
 *
 * Decode hash number into a bound box matches it. Data returned in a four-element array: [minlat, minlon, maxlat, maxlon]
 * @param {Number} hashInt
 * @param {Number} bitDepth
 * @returns {Array}
 */
var decode_bbox_int = function (hashInt, bitDepth) {
  bitDepth = bitDepth || 52

  var maxLat = MAX_LAT,
    minLat = MIN_LAT,
    maxLon = MAX_LON,
    minLon = MIN_LON

  var latBit = 0, lonBit = 0
  var step = bitDepth / 2

  for (var i = 0; i < step; i++) {
    lonBit = get_bit(hashInt, (step - i) * 2 - 1)
    latBit = get_bit(hashInt, (step - i) * 2 - 2)

    if (latBit === 0) {
      maxLat = (maxLat + minLat) / 2
    } else {
      minLat = (maxLat + minLat) / 2
    }

    if (lonBit === 0) {
      maxLon = (maxLon + minLon) / 2
    } else {
      minLon = (maxLon + minLon) / 2
    }
  }
  return [minLat, minLon, maxLat, maxLon]
}

function get_bit(bits, position) {
  return (bits / Math.pow(2, position)) & 0x01
}

/**
 * Decode
 *
 * Decode a hash string into pair of latitude and longitude. A javascript object is returned with keys `latitude`,
 * `longitude` and `error`.
 * @param {String} hashString
 * @returns {Object}
 */
var decode = function (hashString) {
  var bbox = decode_bbox(hashString)
  var lat = (bbox[0] + bbox[2]) / 2
  var lon = (bbox[1] + bbox[3]) / 2
  var latErr = bbox[2] - lat
  var lonErr = bbox[3] - lon
  return {
    latitude: lat,
    longitude: lon,
    error: { latitude: latErr, longitude: lonErr },
  }
}

/**
 * Decode Integer
 *
 * Decode a hash number into pair of latitude and longitude. A javascript object is returned with keys `latitude`,
 * `longitude` and `error`.
 * @param {Number} hash_int
 * @param {Number} bitDepth
 * @returns {Object}
 */
var decode_int = function (hash_int, bitDepth) {
  var bbox = decode_bbox_int(hash_int, bitDepth)
  var lat = (bbox[0] + bbox[2]) / 2
  var lon = (bbox[1] + bbox[3]) / 2
  var latErr = bbox[2] - lat
  var lonErr = bbox[3] - lon
  return {
    latitude: lat,
    longitude: lon,
    error: { latitude: latErr, longitude: lonErr }
  }
}

/**
 * Neighbor
 *
 * Find neighbor of a geohash string in certain direction. Direction is a two-element array, i.e. [1,0] means north, [-1,-1] means southwest.
 * direction [lat, lon], i.e.
 * [1,0] - north
 * [1,1] - northeast
 * ...
 * @param {String} hashString
 * @param {Array} Direction as a 2D normalized vector.
 * @returns {String}
 */
var neighbor = function (hashString, direction) {
  var lonLat = decode(hashString)
  var neighborLat = lonLat.latitude + direction[0] * lonLat.error.latitude * 2
  var neighborLon = lonLat.longitude + direction[1] * lonLat.error.longitude * 2
  neighborLon = ensure_valid_lon(neighborLon)
  neighborLat = ensure_valid_lat(neighborLat)
  return encode(neighborLat, neighborLon, hashString.length)
}

/**
 * Neighbor Integer
 *
 * Find neighbor of a geohash integer in certain direction. Direction is a two-element array, i.e. [1,0] means north, [-1,-1] means southwest.
 * direction [lat, lon], i.e.
 * [1,0] - north
 * [1,1] - northeast
 * ...
 * @param {String} hash_string
 * @returns {Array}
 */
var neighbor_int = function (hash_int, direction, bitDepth) {
  bitDepth = bitDepth || 52
  var lonlat = decode_int(hash_int, bitDepth)
  var neighbor_lat = lonlat.latitude + direction[0] * lonlat.error.latitude * 2
  var neighbor_lon = lonlat.longitude + direction[1] * lonlat.error.longitude * 2
  neighbor_lon = ensure_valid_lon(neighbor_lon)
  neighbor_lat = ensure_valid_lat(neighbor_lat)
  return encode_int(neighbor_lat, neighbor_lon, bitDepth)
}

/**
 * Neighbors
 *
 * Returns all neighbors" hashstrings clockwise from north around to northwest
 * 7 0 1
 * 6 x 2
 * 5 4 3
 * @param {String} hash_string
 * @returns {encoded neighborHashList|Array}
 */
var neighbors = function (hash_string) {
  var hashstringLength = hash_string.length

  var lonlat = decode(hash_string)
  var lat = lonlat.latitude
  var lon = lonlat.longitude
  var latErr = lonlat.error.latitude * 2
  var lonErr = lonlat.error.longitude * 2

  var neighbor_lat, neighbor_lon

  var neighborHashList = [
    encodeNeighbor(1, 0),
    encodeNeighbor(1, 1),
    encodeNeighbor(0, 1),
    encodeNeighbor(-1, 1),
    encodeNeighbor(-1, 0),
    encodeNeighbor(-1, -1),
    encodeNeighbor(0, -1),
    encodeNeighbor(1, -1)
  ]

  function encodeNeighbor(neighborLatDir, neighborLonDir) {
    neighbor_lat = lat + neighborLatDir * latErr
    neighbor_lon = lon + neighborLonDir * lonErr
    neighbor_lon = ensure_valid_lon(neighbor_lon)
    neighbor_lat = ensure_valid_lat(neighbor_lat)
    return encode(neighbor_lat, neighbor_lon, hashstringLength)
  }

  return neighborHashList
}

/**
 * Neighbors Integer
 *
 * Returns all neighbors" hash integers clockwise from north around to northwest
 * 7 0 1
 * 6 x 2
 * 5 4 3
 * @param {Number} hash_int
 * @param {Number} bitDepth
 * @returns {encode_int"d neighborHashIntList|Array}
 */
var neighbors_int = function (hash_int, bitDepth) {
  bitDepth = bitDepth || 52

  var lonlat = decode_int(hash_int, bitDepth)
  var lat = lonlat.latitude
  var lon = lonlat.longitude
  var latErr = lonlat.error.latitude * 2
  var lonErr = lonlat.error.longitude * 2

  var neighbor_lat, neighbor_lon

  var neighborHashIntList = [
    encodeNeighbor_int(1, 0),
    encodeNeighbor_int(1, 1),
    encodeNeighbor_int(0, 1),
    encodeNeighbor_int(-1, 1),
    encodeNeighbor_int(-1, 0),
    encodeNeighbor_int(-1, -1),
    encodeNeighbor_int(0, -1),
    encodeNeighbor_int(1, -1)
  ]

  function encodeNeighbor_int(neighborLatDir, neighborLonDir) {
    neighbor_lat = lat + neighborLatDir * latErr
    neighbor_lon = lon + neighborLonDir * lonErr
    neighbor_lon = ensure_valid_lon(neighbor_lon)
    neighbor_lat = ensure_valid_lat(neighbor_lat)
    return encode_int(neighbor_lat, neighbor_lon, bitDepth)
  }

  return neighborHashIntList
}

/**
 * Bounding Boxes
 *
 * Return all the hashString between minLat, minLon, maxLat, maxLon in numberOfChars
 * @param {Number} minLat
 * @param {Number} minLon
 * @param {Number} maxLat
 * @param {Number} maxLon
 * @param {Number} numberOfChars
 * @returns {bboxes.hashList|Array}
 */
var bboxes = function (minLat, minLon, maxLat, maxLon, numberOfChars) {
  if (numberOfChars <= 0) {
    throw new Error("numberOfChars must be strictly positive")
  }
  numberOfChars = numberOfChars || 9

  var hashSouthWest = encode(minLat, minLon, numberOfChars)
  var hashNorthEast = encode(maxLat, maxLon, numberOfChars)

  var latLon = decode(hashSouthWest)

  var perLat = latLon.error.latitude * 2
  var perLon = latLon.error.longitude * 2

  var boxSouthWest = decode_bbox(hashSouthWest)
  var boxNorthEast = decode_bbox(hashNorthEast)

  var latStep = Math.round((boxNorthEast[0] - boxSouthWest[0]) / perLat)
  var lonStep = Math.round((boxNorthEast[1] - boxSouthWest[1]) / perLon)

  var hashList = []

  for (var lat = 0; lat <= latStep; lat++) {
    for (var lon = 0; lon <= lonStep; lon++) {
      hashList.push(neighbor(hashSouthWest, [lat, lon]))
    }
  }

  return hashList
}

/**
 * Bounding Boxes Integer
 *
 * Return all the hash integers between minLat, minLon, maxLat, maxLon in bitDepth
 * @param {Number} minLat
 * @param {Number} minLon
 * @param {Number} maxLat
 * @param {Number} maxLon
 * @param {Number} bitDepth
 * @returns {bboxes_int.hashList|Array}
 */
var bboxes_int = function (minLat, minLon, maxLat, maxLon, bitDepth) {
  bitDepth = bitDepth || 52

  var hashSouthWest = encode_int(minLat, minLon, bitDepth)
  var hashNorthEast = encode_int(maxLat, maxLon, bitDepth)

  var latlon = decode_int(hashSouthWest, bitDepth)

  var perLat = latlon.error.latitude * 2
  var perLon = latlon.error.longitude * 2

  var boxSouthWest = decode_bbox_int(hashSouthWest, bitDepth)
  var boxNorthEast = decode_bbox_int(hashNorthEast, bitDepth)

  var latStep = Math.round((boxNorthEast[0] - boxSouthWest[0]) / perLat)
  var lonStep = Math.round((boxNorthEast[1] - boxSouthWest[1]) / perLon)

  var hashList = []

  for (var lat = 0; lat <= latStep; lat++) {
    for (var lon = 0; lon <= lonStep; lon++) {
      hashList.push(neighbor_int(hashSouthWest, [lat, lon], bitDepth))
    }
  }

  return hashList
}

function ensure_valid_lon(lon) {
  if (lon > MAX_LON) return MIN_LON + (lon % MAX_LON)
  if (lon < MIN_LON) return MAX_LON + (lon % MAX_LON)
  return lon
}

function ensure_valid_lat(lat) {
  if (lat > MAX_LAT) return MAX_LAT
  if (lat < MIN_LAT) return MIN_LAT
  return lat
}

// 静态网格相关计算方法拓展--------------------------------------------------------------------------------------start

export const NUM_OF_CHARS = 7 // 静态网格计算所用 geohash 位数

export const ORIGIN_GEOHASH = "v9s1psg" // 中国范围内计算静态网格所用的参考点hash值,在中国范围的左上角

// 获取水平方向下一个静态网格的左上角geohash,
// 入参:当前静态网格左上角geohash,视图右侧最大经度值,静态网格尺寸(10、14、20)
// 出参:水平方向下一个静态网格的左上角geohash
function getHorizontalNextStaticGeohash(hashString, maxLon, stepCount) {
  if (!hashString) return false
  const rightNextGeohash = neighbor(hashString, [0, stepCount])
  const bBox = decode_bbox(rightNextGeohash) //[minLat, minLon, maxLat, maxLon]
  if (bBox[1] > maxLon) {
    return false
  } else {
    return rightNextGeohash
  }
}

// 获取垂直方向下一个静态网格的左上角geohash
// 入参:当前静态网格左上角geohash,视图下方最小纬度值,静态网格尺寸(10、14、20)
// 出参:垂直方向下一个静态网格的左上角geohash
function getVerticalNextStaticGeohash(hashString, minLat, stepCount) {
  if (!hashString) return false
  const bottomNextGeohash = neighbor(hashString, [-stepCount, 0])
  const bBox = decode_bbox(bottomNextGeohash) //[minLat, minLon, maxLat, maxLon]
  if (bBox[2] < minLat) {
    return false
  } else {
    return bottomNextGeohash
  }
}
// 根据静态网格左上角geohash和静态网格尺寸,计算出静态网格的paths点阵围栏
function getStaticPaths(hashString, stepCount) {
  const leftTop = hashString
  // const lb = neighbor(hashString, [0, 9]);
  // const rt = neighbor(hashString, [0, 9]);
  const rightBottom = neighbor(hashString, [-(stepCount - 1), stepCount - 1])

  const bBox_lt = decode_bbox(leftTop) //[minLat, minLon, maxLat, maxLon]
  const bBox_rb = decode_bbox(rightBottom) //[minLat, minLon, maxLat, maxLon]

  const minLat = bBox_rb[0]
  const minLon = bBox_lt[1]
  const maxLat = bBox_lt[2]
  const maxLon = bBox_rb[3]

  const paths = [
    [minLat, minLon],
    [minLat, maxLon],
    [maxLat, maxLon],
    [maxLat, minLon]
  ]
  const center = [(maxLat + minLat) / 2, (maxLon + minLon) / 2]
  return { paths, center }
}
// 获取视图范围内所有静态网格数据:hash:左上角geohash,paths:静态网格点阵围栏
function getStaticList(firstGeohash, maxLon, minLat, stepCount) {
  let staticList = [{ hash: firstGeohash, ...getStaticPaths(firstGeohash, stepCount) }]
  let current_h = firstGeohash
  do {
    let current_v = current_h
    do {
      current_v = getVerticalNextStaticGeohash(current_v, minLat, stepCount)
      current_v &&
        staticList.push({
          hash: current_v,
          ...getStaticPaths(current_v, stepCount)
        })
    } while (current_v)
    current_h = getHorizontalNextStaticGeohash(current_h, maxLon, stepCount)
    current_h &&
      staticList.push({
        hash: current_h,
        ...getStaticPaths(current_h, stepCount)
      })
  } while (current_h)
  // console.log(staticList);
  return staticList
}

// 通过 经纬度坐标点 获取该点所在静态网格的左上角geohash
// 入参:计算点、静态网格尺寸
function getLeftTopGeohashOfStaticByPoint(LatLngPoint, stepCount) {
  const { lat, lng } = LatLngPoint
  const leftTopGeohash = encode(lat, lng, NUM_OF_CHARS) // wx4kpt1
  const direction = getDirection(leftTopGeohash, stepCount) // [15,-11]
  const firstGeohash = neighbor(leftTopGeohash, direction)
  return firstGeohash
}

// 静态网格尺寸(10、14、20),计算当前geohash所在静态网格的位置
// 返回 direction [lat, lon] ---- [15,-11]
function getDirection(hashString, stepCount) {
  const currentPonit = decode(hashString) //originPonit.latitude, originPonit.longitude

  let rightNext = ORIGIN_GEOHASH
  let rightNextPonit = decode(rightNext) //originPonit.latitude, originPonit.longitude
  let count_h = 0 // 水平方向计数,当前geohash与originGeohash间隔的hash个数
  do {
    rightNext = neighbor(rightNext, [0, 1])
    rightNextPonit = decode(rightNext)
    count_h++
  } while (rightNextPonit.longitude < currentPonit.longitude)
  // console.log(count_h, count_h % stepCount);

  let bottomNext = ORIGIN_GEOHASH
  let bottomNextPonit = decode(bottomNext) //originPonit.latitude, originPonit.longitude
  let count_v = 0 // 垂直方向计数,当前geohash与originGeohash间隔的hash个数
  do {
    bottomNext = neighbor(bottomNext, [-1, 0])
    bottomNextPonit = decode(bottomNext)
    count_v++
  } while (bottomNextPonit.latitude > currentPonit.latitude)
  // console.log(count_v, count_v % stepCount);

  return [count_v % stepCount, -(count_h % stepCount)]
}

// 计算某矩形范围的左上角geohash所在静态网格的位置,
// 计算出该静态网格左上角第一个geohash,
// 从某矩形范围的左上角第一个静态网格开始,计算出 该矩形范围内的所有 静态网格数据
// 出参:[{hash,  paths, center},...]
function getStaticListByBounds(LatLngBounds, stepCount) {
  const { _northEast, _southWest } = LatLngBounds
  const minLat = _southWest.lat
  const maxLon = _northEast.lng
  // 矩形范围左上角经纬度坐标点
  const point = { lat: _northEast.lat, lng: _southWest.lng }
  // 矩形范围左上角经纬度坐标点----计算----所在静态网格的左上角的geohash
  const firstGeohash = getLeftTopGeohashOfStaticByPoint(point, stepCount)
  // 矩形范围内所有的静态网的
  const staticList = getStaticList(firstGeohash, maxLon, minLat, stepCount)
  return staticList
}

// 根据静态网格的尺寸和左上角的geohash,计算出该静态网格内的所有geohash-----自测用
function getAllHashInStatic(hashString, stepCount) {
  const hashList = []
  // 水平方向 horizontal
  for (let h = 0; h < stepCount; h++) {
    // 垂直方向 vertical
    for (let v = 0; v < stepCount; v++) {
      // hashString反解出所在区域的 最大、最小经度  和 最大、最小纬度  [minLat, minLon, maxLat, maxLon]
      const bBox = decode_bbox(hashString)
      hashList.push({
        hash: neighbor(hashString, [-v, h]),
        paths: [
          [bBox[0], bBox[1]],
          [bBox[0], bBox[3]],
          [bBox[2], bBox[3]],
          [bBox[2], bBox[1]]
        ]
      })
    }
  }
  return hashList
}
// 静态网格相关计算方法拓展-----------------------------------------------------------------------------------------end

var geohash = {
  ENCODE_AUTO: ENCODE_AUTO,
  encode: encode,
  encode_uint64: encode_int, // keeping for backwards compatibility, will deprecate
  encode_int: encode_int,
  decode: decode,
  decode_int: decode_int,
  decode_uint64: decode_int, // keeping for backwards compatibility, will deprecate
  decode_bbox: decode_bbox,
  decode_bbox_uint64: decode_bbox_int, // keeping for backwards compatibility, will deprecate
  decode_bbox_int: decode_bbox_int,
  neighbor: neighbor,
  neighbor_int: neighbor_int,
  neighbors: neighbors,
  neighbors_int: neighbors_int,
  bboxes: bboxes,
  bboxes_int: bboxes_int,
  getHorizontalNextStaticGeohash: getHorizontalNextStaticGeohash,
  getVerticalNextStaticGeohash: getVerticalNextStaticGeohash,
  getStaticPaths: getStaticPaths,
  getStaticList: getStaticList,
  getStaticListByBounds: getStaticListByBounds,
  getDirection: getDirection,
  getAllHashInStatic: getAllHashInStatic
}

//module.exports = geohash;

export default geohash

上一篇下一篇

猜你喜欢

热点阅读