Skip to content

Commit

Permalink
Fix bugs with constructing spatial queries
Browse files Browse the repository at this point in the history
Relates to #2069
  • Loading branch information
robyngit committed Apr 5, 2023
1 parent 84a6183 commit 261025d
Show file tree
Hide file tree
Showing 8 changed files with 471 additions and 245 deletions.
7 changes: 5 additions & 2 deletions src/js/collections/SolrResults.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ define(['jquery', 'underscore', 'backbone', 'models/SolrHeader', 'models/SolrRes
this.header.set({"rows" : solr.responseHeader.params.rows});

//Get the facet counts and store them in this model
if( solr.facet_counts ){
if (solr.facet_counts) {
this.facetCounts = solr.facet_counts.facet_fields;
} else {
this.facetCounts = "nothing";
Expand Down Expand Up @@ -233,7 +233,10 @@ define(['jquery', 'underscore', 'backbone', 'models/SolrHeader', 'models/SolrRes
this.trigger("change:sort");
},

setFacet: function(fields) {
setFacet: function (fields) {
if (!Array.isArray(fields)) {
fields = [fields];
}
this.facet = fields;
this.trigger("change:facet");
},
Expand Down
257 changes: 161 additions & 96 deletions src/js/collections/maps/Geohashes.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ define([
"models/maps/Geohash",
], function ($, _, Backbone, nGeohash, Geohash) {
/**
* @classdesc A Geohashes Collection represents a collection of Geohash models.
* @classdesc A collection of adjacent geohashes, potentially at mixed
* precision levels.
* @classcategory Collections/Geohashes
* @class Geohashes
* @name Geohashes
Expand Down Expand Up @@ -36,16 +37,15 @@ define([
* @returns {number} Length of the geohash.
*/
comparator: function (model) {
return model.get("geohash")?.length || 0;
return model.get("hashString")?.length || 0;
},

/**
* Get the geohash level to use for a given height.
* @param {number} [height] - Altitude to use to calculate the geohash
* level/precision, in meters.
* @returns {number} Geohash level.
* Get the precision height map.
* @returns {Object} Precision height map, where the key is the geohash
* precision level and the value is the height in meters.
*/
getLevelHeightMap: function () {
getPrecisionHeightMap: function () {
return {
1: 6800000,
2: 2400000,
Expand All @@ -57,55 +57,103 @@ define([
},

/**
* Get the geohash level to use for a given height.
*
* Get the geohash precision level to use for a given height.
* @param {number} [height] - Altitude to use to calculate the geohash
* level/precision.
* precision, in meters.
* @returns {number} Geohash precision level.
*/
heightToLevel: function (height) {
heightToPrecision: function (height) {
try {
const levelHeightMap = this.getLevelHeightMap();
return Object.keys(levelHeightMap).find(
(key) => height >= levelHeightMap[key]
const precisionHeightMap = this.getPrecisionHeightMap();
let precision = Object.keys(precisionHeightMap).find(
(key) => height >= precisionHeightMap[key]
);
return precision ? parseInt(precision) : 1;
} catch (e) {
console.log("Failed to get geohash level, returning 1" + e);
console.log("Failed to get geohash precision, returning 1" + e);
return 1;
}
},

/**
* Retrieves the geohash IDs for the provided bounding boxes and level.
*
* Checks if the geohashes in this model are empty or if there are no
* models
* @returns {boolean} True if this collection is empty.
*/
isEmpty: function () {
return (
this.length === 0 || this.models.every((model) => model.isEmpty())
);
},

/**
* Returns true if the set of geohashes in this model collection are the
* 32 geohashes at precision 1, i.e. [0-9a-v]
* @returns {boolean} True if there are 32 geohashes with one character
* each.
*/
isCompleteRootLevel: function () {
const hashStrings = this.getAllHashStrings();
if (hashStrings.length !== 32) return false;
if (hashStrings.some((hash) => hash.length !== 1)) return false;
return true;
},

/**
* Returns true if the geohashes in this model cover the entire earth.
* @returns {boolean} True if the geohashes cover the entire earth.
*/
coversEarth: function () {
if (this.isEmpty()) return false;
if (this.isCompleteRootLevel()) return true;
return this.clone().consolidate().isCompleteRootLevel();
},

/**
* Creates hashStrings for geohashes that are within the provided bounding
* boxes at the given precision. The returned hashStrings are not
* necessarily in the collection.
* @param {Object} bounds - Bounding box with north, south, east, and west
* properties.
* @param {number} level - Geohash level.
* @returns {string[]} Array of geohash IDs.
* @param {number} precision - Geohash precision level.
* @returns {string[]} Array of geohash hashStrings.
*/
getGeohashIDs: function (bounds, level) {
let geohashIDs = [];
getHashStringsByExtent: function (bounds, precision) {
let hashStrings = [];
bounds = this.splitBoundingBox(bounds);
bounds.forEach(function (bb) {
geohashIDs = geohashIDs.concat(
nGeohash.bboxes(bb.south, bb.west, bb.north, bb.east, level)
hashStrings = hashStrings.concat(
nGeohash.bboxes(bb.south, bb.west, bb.north, bb.east, precision)
);
});
return geohashIDs;
return hashStrings;
},

/**
* Returns a list of hashStrings in this collection. Optionally provide a
* precision to only return hashes of that length.
* @param {Number} precision - Geohash precision level.
* @returns {string[]} Array of geohash hashStrings.
*/
getAllHashStrings: function (precision) {
const hashes = this.map((geohash) => geohash.get("hashString"));
if (precision) {
return hashes.filter((hash) => hash.length === precision);
} else {
return hashes;
}
},

/**
* Splits the bounding box if it crosses the prime meridian. Returns an
* array of bounding boxes.
*
* Splits a given bounding box if it crosses the prime meridian. Returns
* an array of bounding boxes.
* @param {Object} bounds - Bounding box object with north, south, east,
* and west properties.
* @returns {Array<Object>} Array of bounding box objects.
* @since x.x.x
* @returns {Object[]} Array of bounding box objects.
*/
splitBoundingBox: function (bounds) {
if (!bounds) return [];
const { north, south, east, west } = bounds;

if (east < west) {
return [
{ north, south, east: 180, west },
Expand All @@ -117,29 +165,35 @@ define([
},

/**
* Add geohashes to the collection based on a bounding box and height.
* Add geohashes to the collection based on a bounding box and height. All
* geohashes within the bounding box at the corresponding precision will
* be added to the collection.
* @param {Object} bounds - Bounding box with north, south, east, and west
* properties.
* @param {number} height - Altitude to use to calculate the geohash
* level/precision.
* @param {number} height - Altitude in meters to use to calculate the
* geohash precision level.
* @param {boolean} [overwrite=false] - Whether to overwrite the current
* collection.
*/
addGeohashesByExtent: function (bounds, height, overwrite = false) {
const level = this.heightToLevel(height);
const geohashIDs = this.getGeohashIDs(bounds, level);
this.addGeohashesById(geohashIDs, overwrite);
const precision = this.heightToPrecision(height);
const hashStrings = this.getHashStringsByExtent(bounds, precision);
this.addGeohashesByHashString(hashStrings, overwrite);
},

/**
* Add geohashes to the collection based on an array of geohash IDs.
* @param {string[]} geohashIDs - Array of geohash IDs.
* Add geohashes to the collection based on an array of geohash
* hashStrings.
* @param {string[]} hashStrings - Array of geohash hashStrings.
* @param {boolean} [overwrite=false] - Whether to overwrite the current
* collection.
*/
addGeohashesById: function (geohashIDs, overwrite = false) {
if (overwrite) this.reset();
this.add(geohashIDs.map((id) => ({ geohash: id })));
addGeohashesByHashString: function (hashStrings, overwrite = false) {
const method = overwrite ? "reset" : "add";
const geohashAttrs = hashStrings.map((gh) => {
return { hashString: gh };
});
this[method](geohashAttrs);
},

/**
Expand All @@ -150,81 +204,101 @@ define([
* @returns {Geohashes} Subset of geohashes.
*/
getSubsetByBounds: function (bounds) {
const levels = this.getLevels();
const precisions = this.getPrecisions();
const hashes = [];
levels.forEach((level) => {
hashes = hashes.concat(this.getGeohashIDs(bounds, level));
precisions.forEach((precision) => {
hashes = hashes.concat(
this.getHashStringsByExtent(bounds, precision)
);
});
const geohashes = this.filter((geohash) => {
return hashes.includes(geohash.get("geohash"));
return hashes.includes(geohash.get("hashString"));
});
return new Geohashes(geohashes);
},

/**
* Check if a geohash is in the collection. This will only consider
* geohash IDs, not properties or any other attributes on the Geohash
* models.
* @param {Geohash} target - Geohash model or geohash hashstring.
* @returns {boolean} Whether the geohash is in the collection.
* geohash hashStrings, not properties or any other attributes on the
* Geohash models.
* @param {Geohash} target - Geohash model or geohash hashString.
* @returns {boolean} Whether the target is part of this collection.
*/
includes: function (geohash) {
const allHashes = this.getGeohashIDs();
const geohashID =
geohash instanceof Geohash ? geohash.get("geohash") : geohash;
return allHashes.includes(geohashID);
const allHashes = this.getAllHashStrings();
const targetHash =
geohash instanceof Geohash ? geohash.get("hashString") : geohash;
return allHashes.includes(targetHash);
},

/**
* Group the geohashes in the collection by their groupID. Their groupID
* is the hashString of the parent geohash, i.e. the hashString of the
* geohash with the last character removed.
* @returns {Object} Object with groupIDs as keys and arrays of Geohash
* models as values.
*/
getGroups: function () {
return this.groupBy((geohash) => {
return geohash.get("groupID");
});
},

/**
* Determine if a set of geohashes can be merged into a single geohash.
* They can be merged if all of the child geohashes are in the collection.
* @param {Geohashes} geohashes - Geohashes collection.
* @param {Geohash} target - Geohash model.
* @returns {boolean} Whether the geohashes can be merged.
* Get the geohash groups in this collection that are complete, i.e. have
* 32 child geohashes.
* @returns {Object} Object with groupIDs as keys and arrays of Geohash
* models as values.
*/
canMerge: function (geohashes, target) {
const children = target.getChildGeohashes();
return children.every((child) => geohashes.includes(child));
getCompleteGroups: function () {
const groups = this.getGroups();
const completeGroups = {};
Object.keys(groups).forEach((groupID) => {
if (groups[groupID].length === 32) {
completeGroups[groupID] = groups[groupID];
}
});
delete completeGroups[""];
delete completeGroups[null];
return completeGroups;
},

/**
* Reduce the set of Geohashes to the minimal set of Geohashes that
* completely cover the same area as the current set. Warning: this will
* remove any properties or attributes from the returned Geohash models.
* @returns {Geohashes} A new Geohashes collection.
* Consolidate this collection: Merge complete groups of geohashes into a
* single, lower precision "parent" geohash. Groups are complete if all 32
* "child" geohashes that make up the "parent" geohash are in the
* collection. Add and remove events will not be triggered during
* consolidation.
*/
getMerged: function () {
// We will merge recursively, so we need to make a copy of the
// collection.
const geohashes = this.clone();
consolidate: function () {
let changed = true;
while (changed) {
changed = false;
geohashes.sort();
for (let i = 0; i < geohashes.length; i++) {
const target = geohashes.at(i);
if (this.canMerge(geohashes, target)) {
const parent = target.getParentGeohash();
const children = target.getChildGeohashes();
geohashes.remove(children);
geohashes.add(parent);
changed = true;
break;
}
}
const toMerge = this.getCompleteGroups();
let toRemove = [];
let toAdd = [];
Object.keys(toMerge).forEach((groupID) => {
const parent = new Geohash({ hashString: groupID });
toRemove = toRemove.concat(toMerge[groupID]);
toAdd.push(parent);
changed = true;
});
this.remove(toRemove, { silent: true });
this.add(toAdd, { silent: true });
}
return geohashes;
return this;
},

/**
* Get the unique geohash levels for all geohashes in the collection.
* Get the unique geohash precision levels present in the collection.
*/
getLevels: function () {
return Array.from(new Set(this.map((geohash) => geohash.get("level"))));
getPrecisions: function () {
return Array.from(new Set(this.map((gh) => gh.get("precision"))));
},

/**
* Return the geohashes as a GeoJSON FeatureCollection, where each
* geohash is represented as a GeoJSON Polygon (rectangle).
* Return the geohashes as a GeoJSON FeatureCollection, where each geohash
* is represented as a GeoJSON Polygon (rectangle).
* @returns {Object} GeoJSON FeatureCollection.
*/
toGeoJSON: function () {
Expand All @@ -240,12 +314,3 @@ define([

return Geohashes;
});

// TODO: consider adding this back in to optionally limit the number of geohashes
// const limit = this.get("maxGeohashes");
// if (limit && geohashIDs.length > limit && level > 1) {
// while (geohashIDs.length > limit && level > 1) {
// level--;
// geohashIDs = this.getGeohashIDs(bounds, level);
// }
// }
3 changes: 2 additions & 1 deletion src/js/models/connectors/Filters-Map.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,8 @@ define([
}

spatialFilters.forEach((spFilter) => {
spFilter.set(extent);
spFilter.set(extent, { silent: true });
spFilter.trigger("change:height");
});
} catch (e) {
console.log("Error updating spatial filters: ", e);
Expand Down
Loading

0 comments on commit 261025d

Please sign in to comment.