lib/geo/DataServiceBboxCache.js
var Class = require('../ext/Class');
var flatten = require('lodash.flatten');
var uniq = require('lodash.uniq');
var DataService = require('../service/data_service/DataService');
var geo = require('../vocab/wgs84');
var NodeFactory = require('../rdf/NodeFactory');
//var tryMergeNode = require('./try-merge-Node');
var shared = require('../util/shared');
var Promise = shared.Promise;
var Bounds = require('./Bounds');
var QuadTree = require('./QuadTree');
var GeoExprUtils = require('./GeoExprUtils');
var tryMergeNode = function() { return false; }; // TODO Implement
var PromiseUtils = require('../util/PromiseUtils');
/**
* Adds a quad tree cache to the lookup service
*/
var DataServiceBboxCache = Class.create(DataService, {
initialize: function(listServiceBbox, maxGlobalItemCount, maxItemsPerTileCount, aquireDepth) {
this.listServiceBbox = listServiceBbox;
var maxBounds = new Bounds(-180.0, -90.0, 180.0, 90.0);
this.quadTree = new QuadTree(maxBounds, 18, 0);
this.maxItemsPerTileCount = maxItemsPerTileCount || 25;
this.maxGlobalItemCount = maxGlobalItemCount || 50;
this.aquireDepth = aquireDepth || 2;
},
// TODO: limit and offset currently ignored
fetchData: function(bounds) {
var result = this.runWorkflow(bounds).then(function(nodes) {
var arrayOfDocs = nodes.map(function(node) {
return node.data.docs;
});
// Remove null items
var docs = arrayOfDocs.filter(function(item) {
return item;
});
docs = flatten(docs, true);
// Add clusters as regular items to the list???
nodes.forEach(function(node) {
if (node.isLoaded) {
return;
}
var wkt = GeoExprUtils.boundsToWkt(node.getBounds());
var cluster = {
key: wkt,
val: {
id: wkt,
// type: 'cluster',
// isZoomCluster: true,
zoomClusterBounds: node.getBounds(),
wkt: wkt // NodeFactory.createPlainLiteral(
}
};
docs.push(cluster);
});
return docs;
});
return result;
},
/*
fetchCount: function(bounds, threshold) {
var result = this.listServiceBbox.fetchCount(bounds, threshold);
return result;
};
*/
runCheckGlobal: function() {
var result;
var rootNode = this.quadTree.getRootNode();
if (!rootNode.checkedGlobal) {
var globalCountTask = this.listServiceBbox.fetchCount(null, this.maxGlobalItemCount);
//console.log('dammit', this.listServiceBbox);
result = globalCountTask.then(function(countInfo) {
var canUseGlobal = !countInfo.hasMoreItems;
//console.log('Global check counts', countInfo);
rootNode.canUseGlobal = canUseGlobal;
rootNode.checkedGlobal = true;
return canUseGlobal;
});
} else {
result = Promise.resolve(rootNode.canUseGlobal).cancellable();
}
return result;
},
runWorkflow: function(bounds) {
var rootNode = this.quadTree.getRootNode();
var self = this;
var result = this.runCheckGlobal().then(function(canUseGlobal) {
//console.log('Can use global? ', canUseGlobal);
var task;
if (canUseGlobal) {
task = self.runGlobalWorkflow(rootNode);
} else {
task = self.runTiledWorkflow(bounds);
}
return task.then(function(nodes) {
return nodes;
});
});
return result;
},
runGlobalWorkflow: function(node) {
var self = this;
var result = this.listServiceBbox.fetchItems(null).then(function(docs) {
// console.log("Global fetching: ", geomToFeatureCount);
self.loadTaskAction(node, docs);
return [
node,
];
});
return result;
},
/**
* This method implements the primary workflow for tile-based fetching
* data.
*
* globalGeomCount = number of geoms - facets enabled, bounds disabled.
* if(globalGeomCount > threshold) {
*
*
* nodes = aquire nodes. foreach(node in nodes) { fetchGeomCount in the
* node - facets TODO enabled or disabled?
*
* nonFullNodes = nodes where geomCount < threshold foreach(node in
* nonFullNodes) { fetch geomToFeatureCount - facets enabled
*
* fetch all positions of geometries in that area -- Optionally:
* fetchGeomToFeatureCount - facets disabled - this can be cached per
* type of interest!! } } }
*
*/
runTiledWorkflow: function(bounds) {
var self = this;
// console.log("Aquiring nodes for " + bounds);
var nodes = this.quadTree.aquireNodes(bounds, this.aquireDepth);
// console.log('Done aquiring');
// Init the data attribute if needed
nodes.forEach(function(node) {
if (!node.data) {
node.data = {};
}
});
// Mark empty nodes as loaded
nodes.forEach(function(node) {
if (node.isCountComplete() && node.infMinItemCount === 0) {
node.isLoaded = true;
}
});
var uncountedNodes = nodes.filter(function(node) {
return self.isCountingNeeded(node);
});
var countTasks = this.createCountTasks(uncountedNodes);
var result = PromiseUtils.all(countTasks).then(function() {
var nonLoadedNodes = nodes.filter(function(node) {
return self.isLoadingNeeded(node);
});
var loadTasks = self.createLoadTasks(nonLoadedNodes);
return PromiseUtils.all(loadTasks).then(function() {
return nodes;
});
});
return result;
},
createCountTask: function(node) {
var self = this;
var threshold = self.maxItemsPerTileCount; // ? self.maxItemsPerTileCount + 1 : null;
var countPromise = this.listServiceBbox.fetchCount(node.getBounds(), threshold);
var result = countPromise.then(function(itemCountInfo) {
var itemCount = itemCountInfo.count;
node.setMinItemCount(itemCountInfo.count);
// If the value is 0, also mark the node as loaded
if (itemCount === 0) {
// self.initNode(node);
node.isLoaded = true;
}
});
return result;
},
/**
* If either the minimum number of items in the node is above the
* threshold or all children have been counted, then there is NO need
* for counting
*
*/
isCountingNeeded: function(node) {
// console.log("Node To Count:", node, node.isCountComplete());
return !(this.isTooManyGeoms(node) || node.isCountComplete());
},
/**
* Loading is needed if NONE of the following criteria applies: . node
* was already loaded . there are no items in the node . there are to
* many items in the node
*
*/
isLoadingNeeded: function(node) {
// (node.data && node.data.isLoaded)
var noLoadingNeeded = node.isLoaded || (node.isCountComplete() && node.infMinItemCount === 0) || this.isTooManyGeoms(node);
return !noLoadingNeeded;
},
isTooManyGeoms: function(node) {
// console.log("FFS", node.infMinItemCount, node.getMinItemCount());
return node.infMinItemCount >= this.maxItemsPerTileCount;
},
createCountTasks: function(nodes) {
var self = this;
var result = nodes.map(function(node) {
return self.createCountTask(node);
}).filter(function(item) {
return item;
});
return result;
},
/**
* Sets the node's state to loaded, attaches the geomToFeatureCount to
* it.
*
* @param {Object} node
* //FIXME: @param {Object} geomToFeatureCount
*/
loadTaskAction: function(node, docs) {
// console.log('Data for ' + node.getBounds() + ': ', docs);
node.data.docs = docs;
node.isLoaded = true;
},
createLoadTasks: function(nodes) {
var self = this;
var result = nodes.map(function(node) {
var loadTask = self.listServiceBbox.fetchItems(node.getBounds()).then(function(docs) {
self.loadTaskAction(node, docs);
});
return loadTask;
});
return result;
},
/**
* TODO Finishing this method at some point to merge nodes together
* could be useful
*
*/
finalizeLoading: function(nodes) {
// Restructure all nodes that have been completely loaded,
var parents = [];
nodes.forEach(function(node) {
if (node.parent) {
parents.push(node.parent);
}
});
parents = uniq(parents);
var each = function(child) {
var indexOf = nodes.indexOf(child);
if (indexOf >= 0) {
nodes[indexOf] = undefined;
}
};
var change = false;
do {
change = false;
for (var i in parents) {
var p = parents[i];
var children = p.children;
var didMerge = tryMergeNode(p);
if (!didMerge) {
continue;
}
change = true;
children.forEach(each);
nodes.push(p);
if (p.parent) {
parents.push(p.parent);
}
break;
}
} while (change === true);
nodes = nodes.filter(function(item) {
return item;
});
/*
* $.each(nodes, function(i, node) { node.isLoaded = true; });
*/
// console.log("All done");
// self._setNodes(nodes, bounds);
// callback.success(nodes, bounds);
},
});
module.exports = DataServiceBboxCache;