forerunnerdb
Version:
A NoSQL document store database for browsers and Node.js.
540 lines (433 loc) • 15.6 kB
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: Index2d.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: Index2d.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>"use strict";
/*
name(string)
id(string)
rebuild(null)
state ?? needed?
match(query, options)
lookup(query, options)
insert(doc)
remove(doc)
primaryKey(string)
collection(collection)
*/
var Shared = require('./Shared'),
Path = require('./Path'),
BinaryTree = require('./BinaryTree'),
GeoHash = require('./GeoHash'),
sharedPathSolver = new Path(),
sharedGeoHashSolver = new GeoHash(),
// GeoHash Distances in Kilometers
geoHashDistance = [
5000,
1250,
156,
39.1,
4.89,
1.22,
0.153,
0.0382,
0.00477,
0.00119,
0.000149,
0.0000372
];
/**
* The index class used to instantiate 2d indexes that the database can
* use to handle high-performance geospatial queries.
* @constructor
*/
var Index2d = function () {
this.init.apply(this, arguments);
};
/**
* Create the index.
* @param {Object} keys The object with the keys that the user wishes the index
* to operate on.
* @param {Object} options Can be undefined, if passed is an object with arbitrary
* options keys and values.
* @param {Collection} collection The collection the index should be created for.
*/
Index2d.prototype.init = function (keys, options, collection) {
this._btree = new BinaryTree();
this._btree.index(keys);
this._size = 0;
this._id = this._itemKeyHash(keys, keys);
this._debug = options && options.debug ? options.debug : false;
this.unique(options && options.unique ? options.unique : false);
if (keys !== undefined) {
this.keys(keys);
}
if (collection !== undefined) {
this.collection(collection);
this._btree.primaryKey(collection.primaryKey());
}
this.name(options && options.name ? options.name : this._id);
this._btree.debug(this._debug);
};
Shared.addModule('Index2d', Index2d);
Shared.mixin(Index2d.prototype, 'Mixin.Common');
Shared.mixin(Index2d.prototype, 'Mixin.ChainReactor');
Shared.mixin(Index2d.prototype, 'Mixin.Sorting');
Index2d.prototype.id = function () {
return this._id;
};
Index2d.prototype.state = function () {
return this._state;
};
Index2d.prototype.size = function () {
return this._size;
};
Shared.synthesize(Index2d.prototype, 'data');
Shared.synthesize(Index2d.prototype, 'name');
Shared.synthesize(Index2d.prototype, 'collection');
Shared.synthesize(Index2d.prototype, 'type');
Shared.synthesize(Index2d.prototype, 'unique');
Index2d.prototype.keys = function (val) {
if (val !== undefined) {
this._keys = val;
// Count the keys
this._keyCount = sharedPathSolver.parse(this._keys).length;
return this;
}
return this._keys;
};
Index2d.prototype.rebuild = function () {
// Do we have a collection?
if (this._collection) {
// Get sorted data
var collection = this._collection.subset({}, {
$decouple: false,
$orderBy: this._keys
}),
collectionData = collection.find(),
dataIndex,
dataCount = collectionData.length;
// Clear the index data for the index
this._btree.clear();
this._size = 0;
if (this._unique) {
this._uniqueLookup = {};
}
// Loop the collection data
for (dataIndex = 0; dataIndex < dataCount; dataIndex++) {
this.insert(collectionData[dataIndex]);
}
}
this._state = {
name: this._name,
keys: this._keys,
indexSize: this._size,
built: new Date(),
updated: new Date(),
ok: true
};
};
Index2d.prototype.insert = function (dataItem, options) {
var uniqueFlag = this._unique,
uniqueHash;
dataItem = this.decouple(dataItem);
if (uniqueFlag) {
uniqueHash = this._itemHash(dataItem, this._keys);
this._uniqueLookup[uniqueHash] = dataItem;
}
// Convert 2d indexed values to geohashes
var keys = this._btree.keys(),
pathVal,
geoHash,
lng,
lat,
i;
for (i = 0; i < keys.length; i++) {
pathVal = sharedPathSolver.get(dataItem, keys[i].path);
if (pathVal instanceof Array) {
lng = pathVal[0];
lat = pathVal[1];
geoHash = sharedGeoHashSolver.encode(lng, lat);
sharedPathSolver.set(dataItem, keys[i].path, geoHash);
}
}
if (this._btree.insert(dataItem)) {
this._size++;
return true;
}
return false;
};
Index2d.prototype.remove = function (dataItem, options) {
var uniqueFlag = this._unique,
uniqueHash;
if (uniqueFlag) {
uniqueHash = this._itemHash(dataItem, this._keys);
delete this._uniqueLookup[uniqueHash];
}
if (this._btree.remove(dataItem)) {
this._size--;
return true;
}
return false;
};
Index2d.prototype.violation = function (dataItem) {
// Generate item hash
var uniqueHash = this._itemHash(dataItem, this._keys);
// Check if the item breaks the unique constraint
return Boolean(this._uniqueLookup[uniqueHash]);
};
Index2d.prototype.hashViolation = function (uniqueHash) {
// Check if the item breaks the unique constraint
return Boolean(this._uniqueLookup[uniqueHash]);
};
/**
* Looks up records that match the passed query and options.
* @param query The query to execute.
* @param options A query options object.
* @param {Operation=} op Optional operation instance that allows
* us to provide operation diagnostics and analytics back to the
* main calling instance as the process is running.
* @returns {*}
*/
Index2d.prototype.lookup = function (query, options, op) {
// Loop the indexed keys and determine if the query has any operators
// that we want to handle differently from a standard lookup
var keys = this._btree.keys(),
pathStr,
pathVal,
results,
i;
for (i = 0; i < keys.length; i++) {
pathStr = keys[i].path;
pathVal = sharedPathSolver.get(query, pathStr);
if (typeof pathVal === 'object') {
if (pathVal.$near) {
results = [];
// Do a near point lookup
results = results.concat(this.near(pathStr, pathVal.$near, options, op));
}
if (pathVal.$geoWithin) {
results = [];
// Do a geoWithin shape lookup
results = results.concat(this.geoWithin(pathStr, pathVal.$geoWithin, options, op));
}
return results;
}
}
return this._btree.lookup(query, options);
};
Index2d.prototype.near = function (pathStr, query, options, op) {
var self = this,
neighbours,
visitedCount,
visitedNodes,
visitedData,
search,
results,
finalResults = [],
precision,
maxDistanceKm,
distance,
distCache,
latLng,
pk = this._collection.primaryKey(),
i;
// Calculate the required precision to encapsulate the distance
// TODO: Instead of opting for the "one size larger" than the distance boxes,
// TODO: we should calculate closest divisible box size as a multiple and then
// TODO: scan neighbours until we have covered the area otherwise we risk
// TODO: opening the results up to vastly more information as the box size
// TODO: increases dramatically between the geohash precisions
if (query.$distanceUnits === 'km') {
maxDistanceKm = query.$maxDistance;
for (i = 0; i < geoHashDistance.length; i++) {
if (maxDistanceKm > geoHashDistance[i]) {
precision = i + 1;
break;
}
}
} else if (query.$distanceUnits === 'miles') {
maxDistanceKm = query.$maxDistance * 1.60934;
for (i = 0; i < geoHashDistance.length; i++) {
if (maxDistanceKm > geoHashDistance[i]) {
precision = i + 1;
break;
}
}
}
if (precision === 0) {
precision = 1;
}
// Calculate 9 box geohashes
if (op) { op.time('index2d.calculateHashArea'); }
neighbours = sharedGeoHashSolver.calculateHashArrayByRadius(query.$point, maxDistanceKm, precision);
if (op) { op.time('index2d.calculateHashArea'); }
if (op) {
op.data('index2d.near.precision', precision);
op.data('index2d.near.hashArea', neighbours);
op.data('index2d.near.maxDistanceKm', maxDistanceKm);
op.data('index2d.near.centerPointCoords', [query.$point[0], query.$point[1]]);
}
// Lookup all matching co-ordinates from the btree
results = [];
visitedCount = 0;
visitedData = {};
visitedNodes = [];
if (op) { op.time('index2d.near.getDocsInsideHashArea'); }
for (i = 0; i < neighbours.length; i++) {
search = this._btree.startsWith(pathStr, neighbours[i]);
visitedData[neighbours[i]] = search;
visitedCount += search._visitedCount;
visitedNodes = visitedNodes.concat(search._visitedNodes);
results = results.concat(search);
}
if (op) {
op.time('index2d.near.getDocsInsideHashArea');
op.data('index2d.near.startsWith', visitedData);
op.data('index2d.near.visitedTreeNodes', visitedNodes);
}
// Work with original data
if (op) { op.time('index2d.near.lookupDocsById'); }
results = this._collection._primaryIndex.lookup(results);
if (op) { op.time('index2d.near.lookupDocsById'); }
if (query.$distanceField) {
// Decouple the results before we modify them
results = this.decouple(results);
}
if (results.length) {
distance = {};
if (op) { op.time('index2d.near.calculateDistanceFromCenter'); }
// Loop the results and calculate distance
for (i = 0; i < results.length; i++) {
latLng = sharedPathSolver.get(results[i], pathStr);
distCache = distance[results[i][pk]] = this.distanceBetweenPoints(query.$point[0], query.$point[1], latLng[0], latLng[1]);
if (distCache <= maxDistanceKm) {
if (query.$distanceField) {
// Options specify a field to add the distance data to
// so add it now
sharedPathSolver.set(results[i], query.$distanceField, query.$distanceUnits === 'km' ? distCache : Math.round(distCache * 0.621371));
}
if (query.$geoHashField) {
// Options specify a field to add the distance data to
// so add it now
sharedPathSolver.set(results[i], query.$geoHashField, sharedGeoHashSolver.encode(latLng[0], latLng[1], precision));
}
// Add item as it is inside radius distance
finalResults.push(results[i]);
}
}
if (op) { op.time('index2d.near.calculateDistanceFromCenter'); }
// Sort by distance from center
if (op) { op.time('index2d.near.sortResultsByDistance'); }
finalResults.sort(function (a, b) {
return self.sortAsc(distance[a[pk]], distance[b[pk]]);
});
if (op) { op.time('index2d.near.sortResultsByDistance'); }
}
// Return data
return finalResults;
};
Index2d.prototype.geoWithin = function (pathStr, query, options) {
console.log('geoWithin() is currently a prototype method with no actual implementation... it just returns a blank array.');
return [];
};
Index2d.prototype.distanceBetweenPoints = function (lat1, lng1, lat2, lng2) {
var R = 6371; // kilometres
var lat1Rad = this.toRadians(lat1);
var lat2Rad = this.toRadians(lat2);
var lat2MinusLat1Rad = this.toRadians(lat2-lat1);
var lng2MinusLng1Rad = this.toRadians(lng2-lng1);
var a = Math.sin(lat2MinusLat1Rad/2) * Math.sin(lat2MinusLat1Rad/2) +
Math.cos(lat1Rad) * Math.cos(lat2Rad) *
Math.sin(lng2MinusLng1Rad/2) * Math.sin(lng2MinusLng1Rad/2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
};
Index2d.prototype.toRadians = function (degrees) {
return degrees * 0.01747722222222;
};
Index2d.prototype.match = function (query, options) {
// TODO: work out how to represent that this is a better match if the query has $near than
// TODO: a basic btree index which will not be able to resolve a $near operator
return this._btree.match(query, options);
};
Index2d.prototype._itemHash = function (item, keys) {
var path = new Path(),
pathData,
hash = '',
k;
pathData = path.parse(keys);
for (k = 0; k < pathData.length; k++) {
if (hash) { hash += '_'; }
hash += path.value(item, pathData[k].path).join(':');
}
return hash;
};
Index2d.prototype._itemKeyHash = function (item, keys) {
var path = new Path(),
pathData,
hash = '',
k;
pathData = path.parse(keys);
for (k = 0; k < pathData.length; k++) {
if (hash) { hash += '_'; }
hash += path.keyValue(item, pathData[k].path);
}
return hash;
};
Index2d.prototype._itemHashArr = function (item, keys) {
var path = new Path(),
pathData,
//hash = '',
hashArr = [],
valArr,
i, k, j;
pathData = path.parse(keys);
for (k = 0; k < pathData.length; k++) {
valArr = path.value(item, pathData[k].path);
for (i = 0; i < valArr.length; i++) {
if (k === 0) {
// Setup the initial hash array
hashArr.push(valArr[i]);
} else {
// Loop the hash array and concat the value to it
for (j = 0; j < hashArr.length; j++) {
hashArr[j] = hashArr[j] + '_' + valArr[i];
}
}
}
}
return hashArr;
};
// Register this index on the shared object
Shared.index['2d'] = Index2d;
Shared.finishModule('Index2d');
module.exports = Index2d;</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="ActiveBucket.html">ActiveBucket</a></li><li><a href="Angular.html">Angular</a></li><li><a href="AutoBind.html">AutoBind</a></li><li><a href="Collection.html">Collection</a></li><li><a href="CollectionGroup.html">CollectionGroup</a></li><li><a href="Condition.html">Condition</a></li><li><a href="Core.html">Core</a></li><li><a href="Db.html">Db</a></li><li><a href="Document.html">Document</a></li><li><a href="Grid.html">Grid</a></li><li><a href="Highchart.html">Highchart</a></li><li><a href="Index2d.html">Index2d</a></li><li><a href="IndexBinaryTree.html">IndexBinaryTree</a></li><li><a href="IndexHashMap.html">IndexHashMap</a></li><li><a href="Infinilist.html">Infinilist</a></li><li><a href="KeyValueStore.html">KeyValueStore</a></li><li><a href="Metrics.html">Metrics</a></li><li><a href="MyModule.html">MyModule</a></li><li><a href="NodeApiClient.html">NodeApiClient</a></li><li><a href="NodeApiServer.html">NodeApiServer</a></li><li><a href="NodeRAS.html">NodeRAS</a></li><li><a href="Odm.html">Odm</a></li><li><a href="OldView.html">OldView</a></li><li><a href="Operation.html">Operation</a></li><li><a href="Overload.html">Overload</a></li><li><a href="Overview.html">Overview</a></li><li><a href="Overview_init.html">init</a></li><li><a href="Path.html">Path</a></li><li><a href="Persist.html">Persist</a></li><li><a href="Procedure.html">Procedure</a></li><li><a href="ReactorIO.html">ReactorIO</a></li><li><a href="Section.html">Section</a></li><li><a href="Serialiser.html">Serialiser</a></li><li><a href="Shared.overload.html">overload</a></li><li><a href="View.html">View</a></li></ul><h3>Mixins</h3><ul><li><a href="ChainReactor.html">ChainReactor</a></li><li><a href="Common.html">Common</a></li><li><a href="Constants.html">Constants</a></li><li><a href="Events.html">Events</a></li><li><a href="Matching.html">Matching</a></li><li><a href="Shared.html">Shared</a></li><li><a href="Sorting.html">Sorting</a></li><li><a href="Tags.html">Tags</a></li><li><a href="Triggers.html">Triggers</a></li><li><a href="Updating.html">Updating</a></li></ul><h3><a href="global.html">Global</a></h3>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.4.0</a> on Thu Mar 01 2018 11:34:22 GMT+0000 (GMT)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>