Skip to content

Instantly share code, notes, and snippets.

@psaia
Created August 10, 2017 19:41
Show Gist options
  • Select an option

  • Save psaia/314e15b954276d4b7633bb8ea18b92e3 to your computer and use it in GitHub Desktop.

Select an option

Save psaia/314e15b954276d4b7633bb8ea18b92e3 to your computer and use it in GitHub Desktop.
const zoomScale = [ // [ radius, min points ]
[0, 10],
[0.9, 10], // 1
[0.7, 10], // 2
[0.6, 10], // 3
[0.5, 10], // 4
[0.4, 9], // 5
[0.3, 8], // 6
[0.13, 3], // 7
[0.09, 3], // 8
[0.07, 2], // 9
[0.05, 2], // 10
[0.0239, 2], // 11
[0.019, 2], // 12
[0.002, 2], // 13
[0.0019, 2], // 14
[0.0008, 5], // 15
[0.0007, 5], // 16
[0.00006, 5], // 17
[0.00000001, 5], // 18
[0.000000001, 5], // 19
[0.0000000001, 5], // 20
[0.00000000001, 5] // 21
];
/**
* This method is used for the map queries only.
*/
schema.statics.spatialFind = function(organization, zoom, bllng, bllat, urlng, urlat, cats, photos, surveys, callback) {
console.time('clustering');
// Minimum number of points in a cluster.
const POINT_NUM = zoomScale[zoom][1];
// Margin radius.
const RADIUS = zoomScale[zoom][0];
const aggregates = [];
const match = {
$match: {
organization: mongoose.Types.ObjectId(organization),
location: {
$exists: true,
$not: {
$size: 0
}
}
}
};
if (_.isArray(cats)) {
match.$match.categories = {
$in: cats.map(mongoose.Types.ObjectId)
};
};
if (surveys && photos) {
match.$match.$or = [
{
survey: {
$elemMatch: {
pub: true
}
}
},
{
photoCount: {
$gt: 0
}
}
];
} else {
if (surveys) {
match.$match.survey = {
$elemMatch: {
pub: true
}
};
} else if (photos) {
match.$match.photoCount = {
$gte: 1
};
}
}
match.$match.location = {
$geoWithin: {
$box: [
[bllng, bllat],
[urlng, urlat]
]
}
};
aggregates.push(
match,
{
$unwind: '$location'
},
{
$group: {
_id: '$_id',
photoCount: {
$sum: '$photoCount'
},
city: {
$first: '$city'
},
state: {
$first: '$state'
},
zip: {
$first: '$zip'
},
street: {
$first: '$street'
},
surveyCompleted: {
$first: '$surveyCompleted'
},
location: {
$push: '$location'
}
}
},
{
$project: {
a: {
city: '$city',
state: '$state',
zip: '$zip',
street: '$street',
},
s: '$surveyCompleted',
p: '$photoCount', // Number of photos.
l: '$location' // Coordinates.
}
}
);
Model.aggregate(aggregates, function(err, results) {
if (err) {
return callback(err);
}
const coords = results.map(pt => [pt.l[1], pt.l[0]]); // [lat, lng]
const completeClusters = [];
const singles = [];
const populateSingles = () => {
for (let i = 0, len = results.length; i < len; i++) {
singles.push(results[i]);
}
};
// To cluster or not to cluster is the question.
if (zoom < 13) {
const dbscan = new clustering.DBSCAN();
const indexClusters = dbscan.run(coords, RADIUS, POINT_NUM);
console.log('result count', results.length);
console.log('noise count', dbscan.noise.length);
console.log('cluster count', indexClusters.length);
for (let i = 0, len = indexClusters.length; i < len; i++) {
const clusterIndexRow = indexClusters[i];
const clusterPointRow = clusterIndexRow.map(i => {
return results[i];
});
const center = centroid(clusterPointRow.map(p => p.l));
completeClusters.push({
pts: clusterPointRow,
c: center
});
}
if (!completeClusters.length) {
populateSingles();
}
} else {
populateSingles();
}
async.parallel([
function(callback) {
Model.count(
{
organization: mongoose.Types.ObjectId(organization),
location: {
$exists: true,
$not: {
$size: 0
}
},
},
callback
);
},
function(callback) {
Model.count(
{
organization: mongoose.Types.ObjectId(organization),
location: {
$exists: true,
$not: {
$size: 0
}
},
survey: {
$elemMatch: {
pub: true
}
}
},
callback
);
},
function(callback) {
Model.count(
{
organization: mongoose.Types.ObjectId(organization),
location: {
$exists: true,
$not: {
$size: 0
}
},
photoCount: {
$gte: 1
}
},
callback
);
}
],
function(err, results) {
console.timeEnd('clustering');
if (err) {
return callback(err);
}
callback(null, {
total: results[0],
hasReviews: results[1],
hasPhotos: results[2],
clusters: completeClusters,
singles: singles
});
});
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment