strikeentco/geo-nearby

View on GitHub
lib/base.js

Summary

Maintainability
B
4 hrs
Test Coverage
'use strict';

const geohash = require('ngeohash');
const fs = require('fs');
const sort = require('./sort');
const helpers = require('./helpers');

const get = helpers.get;
const getFeatureCollection = helpers.getFeatureCollection;
const getCoords = helpers.getCoords;
const isArray = helpers.isArray;
const isGeoJSON = helpers.isGeoJSON;
const uniq = helpers.uniq;
const rangeIndex = helpers.rangeIndex;
const rangeIndexLength = rangeIndex.length;
const binarySearch = helpers.binarySearch;

module.exports.createCompactSet = (data, opts) => {
  if (typeof data === 'string') {
    try {
      data = JSON.parse(data);
    } catch (e) {
      throw new TypeError('data must be correct JSON or GeoJSON');
    }
  }

  let geo = [];

  if (isArray(data)) {
    if (data[0].i && data[0].g) {
      return sort(data);
    }
    data.forEach(item => {
      let g;
      if (opts.hash) {
        g = get(item, opts.hash);
      }
      g = g || geohash.encode_int(get(item, opts.lat), get(item, opts.lon));
      geo.push({
        i: get(item, opts.id),
        g
      });
    });
  } else if (isGeoJSON(data)) {
    const features = getFeatureCollection(data);
    features.forEach(feature => {
      const geometry = feature.geometry;
      const coords = getCoords(geometry);
      if (coords) {
        const properties = feature.properties;
        geo.push({
          i: get(properties, opts.id),
          g: geohash.encode_int(coords.lat, coords.lon)
        });
      }
    });
  } else {
    throw new TypeError('data must be correct JSON or GeoJSON');
  }

  geo = sort(geo);

  if (opts.file) {
    fs.writeFileSync(opts.file, JSON.stringify(geo));
  }

  return geo;
};

function rangeDepth(radius) {
  for (let i = 0; i < rangeIndexLength - 1; i++) {
    if (radius - rangeIndex[i] < rangeIndex[i + 1] - radius) {
      return 52 - (i * 2);
    }
  }

  return 2;
}

function buildBoxSet(hash, radiusBitDepth) {
  const neighbors = geohash.neighbors_int(hash, radiusBitDepth);

  neighbors.push(hash);
  neighbors.sort();

  return uniq(neighbors);
}

function leftShift(hash, bit) {
  return hash * Math.pow(2, bit);
}

function rangeSet(lat, lon, radiusBitDepth, bitDepth) {
  const hash = geohash.encode_int(lat, lon, radiusBitDepth);
  const neighbors = buildBoxSet(hash, radiusBitDepth);
  const bitDiff = bitDepth - radiusBitDepth;

  const ranges = [];
  let lowerRange = 0;
  let upperRange = 0;

  for (let i = 0; i < neighbors.length; i++) {
    lowerRange = neighbors[i];
    upperRange = lowerRange + 1;
    while (neighbors[i + 1] === upperRange) {
      neighbors.shift();
      upperRange = neighbors[i] + 1;
    }

    ranges.push({ lower: leftShift(lowerRange, bitDiff), upper: leftShift(upperRange, bitDiff) });
  }

  return ranges;
}

function searchBetween(set, min, max, limit) {
  const hash = set.hash;
  const data = set.data;
  const result = [];
  const length = data.length;

  if (set._sorted) {
    const _min = binarySearch(set, min, 0, length - 1);
    const _max = binarySearch(set, max, 0, length - 1, true);
    for (let i = _min; i <= _max; i++) {
      if (limit && result.length >= limit) {
        return result;
      }
      result.push(data[i]);
    }

    return result;
  }

  // eslint-disable-next-line no-var, vars-on-top
  for (var i = 0; i < length; i++) { // Low performance when use "let" in "for" loop - https://bugs.chromium.org/p/v8/issues/detail?id=4762
    const value = data[i];
    if (value[hash] >= min && value[hash] <= max) {
      if (limit && result.length >= limit) {
        return result;
      }
      result.push(value);
    }
  }

  return result;
}

function queryByRanges(data, ranges, limit) {
  let replies = [];
  for (let i = 0; i < ranges.length; i++) {
    const range = ranges[i];
    const replie = searchBetween(data, range.lower, range.upper, limit);
    if (replie.length) {
      replies = replies.concat(replie);
    }
    if (limit && replies.length >= limit) {
      return replies;
    }
  }

  return replies;
}

module.exports.nearBy = (data, opts) =>
  queryByRanges(data, rangeSet(opts.lat, opts.lon, rangeDepth(opts.radius), 52), opts.limit);