From 2d8c90786fb1939a1f952eae51f76a148a203997 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 11 Jan 2021 13:01:30 -0500 Subject: [PATCH] coreLocation tests, documentation --- modules/core/index.js | 2 +- modules/core/locations.js | 79 ++++++++++++------- test/index.html | 3 +- test/spec/core/locations.js | 150 ++++++++++++++++++++++++++++++++++++ 4 files changed, 204 insertions(+), 30 deletions(-) create mode 100644 test/spec/core/locations.js diff --git a/modules/core/index.js b/modules/core/index.js index c998e1568..98f01b97e 100644 --- a/modules/core/index.js +++ b/modules/core/index.js @@ -4,7 +4,7 @@ export { coreDifference } from './difference'; export { coreGraph } from './graph'; export { coreHistory } from './history'; export { coreLocalizer, t, localizer } from './localizer'; -export { coreLocations } from './locations'; +export { coreLocations, locationManager } from './locations'; export { prefs } from './preferences'; export { coreTree } from './tree'; export { coreUploader } from './uploader'; diff --git a/modules/core/locations.js b/modules/core/locations.js index fe1fd4889..83b4acad8 100644 --- a/modules/core/locations.js +++ b/modules/core/locations.js @@ -22,9 +22,9 @@ export { _mainLocations as locationManager }; // export function coreLocations() { let _this = {}; - let _resolvedFeatures = {}; // cache of *resolved* locationSet features - let _loco = new LocationConflation(); // instance of a location-conflation resolver - let _wp; // instance of a which-polygon index + let _resolvedFeatures = {}; // cache of *resolved* locationSet features + let _loco = new LocationConflation(); // instance of a location-conflation resolver + let _wp; // instance of a which-polygon index // pre-resolve the worldwide locationSet const world = { locationSet: { include: ['Q2'] } }; @@ -36,6 +36,7 @@ export function coreLocations() { let _inProcess; + // Returns a Promise to process the queue function processQueue() { if (!_queue.length) return Promise.resolve(); @@ -55,6 +56,8 @@ export function coreLocations() { .then(() => processQueue()); } + // Pass an Object with a `locationSet` property, + // Performs the locationSet resolution, caches the result, and sets a `locationSetID` property on the object. function resolveLocationSet(obj) { if (obj.locationSetID) return; // work was done already @@ -80,6 +83,7 @@ export function coreLocations() { } } + // Rebuilds the whichPolygon index with whatever features have been resolved. function rebuildIndex() { _wp = whichPolygon({ features: Object.values(_resolvedFeatures) }); } @@ -148,7 +152,7 @@ export function coreLocations() { // ] // // Returns a Promise fullfilled when the resolving/indexing has been completed - // This will take some seconds but happen in the background during browser idle time + // This will take some seconds but happen in the background during browser idle time. // _this.mergeLocationSets = (objects) => { if (!Array.isArray(objects)) return Promise.reject('nothing to do'); @@ -165,12 +169,12 @@ export function coreLocations() { // https://github.com/osmlab/name-suggestion-index/issues/4784#issuecomment-742003434 _queue = _queue.concat(utilArrayChunk(objects, 200)); - // Everything after here will be deferred. if (!_inProcess) { _inProcess = processQueue() .then(() => { rebuildIndex(); _inProcess = null; + return objects; }); } return _inProcess; @@ -179,7 +183,13 @@ export function coreLocations() { // // `locationSetID` - // Return a locationSetID for a given locationSet (fallback to the 'world') + // Returns a locationSetID for a given locationSet (fallback to `+[Q2]`, world) + // (The locationset doesn't necessarily need to be resolved to compute its `id`) + // + // Arguments + // `locationSet`: A locationSet, e.g. `{ include: ['us'] }` + // Returns + // The locationSetID, e.g. `+[Q30]` // _this.locationSetID = (locationSet) => { let locationSetID; @@ -194,36 +204,36 @@ export function coreLocations() { // // `feature` - // Return the GeoJSON feature for a given locationSetID (fallback to 'world') + // Returns the resolved GeoJSON feature for a given locationSetID (fallback to 'world') // + // Arguments + // `locationSetID`: id of the form like `+[Q30]` (United States) + // Returns + // A GeoJSON feature: + // { + // type: 'Feature', + // id: '+[Q30]', + // properties: { id: '+[Q30]', area: 21817019.17, … }, + // geometry: { … } + // } _this.feature = (locationSetID) => _resolvedFeatures[locationSetID] || _resolvedFeatures['+[Q2]']; - // - // `query` - // Execute a query directly against which-polygon - // https://github.com/mapbox/which-polygon - // Arguments - // `loc`: the [lon,lat] location to query, - // `multi`= true to return all results, `false` to return first result - // Returns - // Array of GeoJSON *properties* for the locationSet features that exist at `loc` - // - _this.query = (loc, multi) => _wp(loc, multi); - // // `locationsAt` - // Convenience method to find all the locationSets valid at the given location. + // Find all the resolved locationSets valid at the given location. + // Results include the area (in km²) to facilitate sorting. + // // Arguments - // `loc`: the [lon,lat] location to query + // `loc`: the [lon,lat] location to query, e.g. `[-74.4813, 40.7967]` // Returns - // A result Object of ids to areas - // { - // "+[Q2]": 511207893.3958111, - // "+[Q30]": 21817019.17, - // "+[new_jersey.geojson]": 22390.77, - // … - // } + // Object of locationSetIDs to areas (in km²) + // { + // "+[Q2]": 511207893.3958111, + // "+[Q30]": 21817019.17, + // "+[new_jersey.geojson]": 22390.77, + // … + // } // _this.locationsAt = (loc) => { let result = {}; @@ -231,6 +241,19 @@ export function coreLocations() { return result; }; + // + // `query` + // Execute a query directly against which-polygon + // https://github.com/mapbox/which-polygon + // + // Arguments + // `loc`: the [lon,lat] location to query, + // `multi`: `true` to return all results, `false` to return first result + // Returns + // Array of GeoJSON *properties* for the locationSet features that exist at `loc` + // + _this.query = (loc, multi) => _wp(loc, multi); + // Direct access to the location-conflation resolver _this.loco = () => _loco; diff --git a/test/index.html b/test/index.html index 69bb3ade4..2e87c89dd 100644 --- a/test/index.html +++ b/test/index.html @@ -81,6 +81,7 @@ 'spec/core/file_fetcher.js', 'spec/core/graph.js', 'spec/core/history.js', + 'spec/core/locations.js', 'spec/core/tree.js', 'spec/core/validator.js', @@ -203,4 +204,4 @@ - + \ No newline at end of file diff --git a/test/spec/core/locations.js b/test/spec/core/locations.js new file mode 100644 index 000000000..4bcca4e65 --- /dev/null +++ b/test/spec/core/locations.js @@ -0,0 +1,150 @@ +describe('iD.coreLocations', function() { + var locationManager, loco, wp; + + var colorado = { + type: 'Feature', + id: 'colorado.geojson', + properties: {}, + geometry: { + type: 'Polygon', + coordinates: [ + [ + [-107.9197, 41.0039], + [-102.0539, 41.0039], + [-102.043, 36.9948], + [-109.0425, 37.0003], + [-109.048, 40.9984], + [-107.9197, 41.0039] + ] + ] + } + }; + + var fc = { type: 'FeatureCollection', features: [colorado] }; + + + beforeEach(function() { + // make a new one each time, so we aren't accidently testing the "global" locationManager + locationManager = iD.coreLocations(); + loco = locationManager.loco(); + wp = locationManager.wp(); + }); + + + describe('#mergeCustomGeoJSON', function() { + it('merges geojson into lococation-conflation cache', function() { + locationManager.mergeCustomGeoJSON(fc); + expect(loco._cache['colorado.geojson']).to.be.eql(colorado); + }); + }); + + + describe('#mergeLocationSets', function() { + it('returns a promise rejected if not passed an array', function(done) { + var prom = locationManager.mergeLocationSets({}); + prom + .then(function() { + throw new Error('This was supposed to fail, but somehow succeeded.'); + }) + .catch(function(err) { + expect(/^nothing to do/.test(err)).to.be.true; + }) + .finally(done); + + window.setTimeout(function() {}, 20); // async - to let the promise settle in phantomjs + }); + + it('resolves locationSets, assigning locationSetID', function(done) { + var data = [ + { id: 'world', locationSet: { include: ['001'] } }, + { id: 'usa', locationSet: { include: ['usa'] } } + ]; + var prom = locationManager.mergeLocationSets(data); + prom + .then(function(data) { + expect(data).to.be.a('array'); + expect(data[0]).locationSetID.to.eql('+[Q2]'); + expect(data[1]).locationSetID.to.eql('+[Q30]'); + }) + .finally(done); + + window.setTimeout(function() {}, 20); // async - to let the promise settle in phantomjs + }); + + it('resolves locationSets, falls back to world locationSetID on errror', function(done) { + var data = [ + { id: 'bogus1', locationSet: { foo: 'bar' } }, + { id: 'bogus2', locationSet: { include: ['fake.geojson'] } } + ]; + var prom = locationManager.mergeLocationSets(data); + prom + .then(function(data) { + expect(data).to.be.a('array'); + expect(data[0]).locationSetID.to.eql('+[Q2]'); + expect(data[1]).locationSetID.to.eql('+[Q2]'); + }) + .finally(done); + + window.setTimeout(function() {}, 20); // async - to let the promise settle in phantomjs + }); + }); + + + describe('#locationSetID', function() { + it('calculates a locationSetID for a locationSet', function() { + expect(locationManager.locationSetID({ include: ['usa'] })).to.be.eql('+[Q30]'); + }); + + it('falls back to the world locationSetID in case of errors', function() { + expect(locationManager.locationSetID({ foo: 'bar' })).to.be.eql('+[Q2]'); + expect(locationManager.locationSetID({ include: ['fake.geojson'] })).to.be.eql('+[Q2]'); + }); + }); + + + describe('#feature', function() { + it('has the world locationSet pre-resolved', function() { + var result = locationManager.feature('+[Q2]'); + expect(result).to.include({ type: 'Feature', id: '+[Q2]' }); + }); + + it('falls back to the world locationSetID in case of errors', function() { + var result = locationManager.feature('fake'); + expect(result).to.include({ type: 'Feature', id: '+[Q2]' }); + }); + }); + + + describe('#locationsAt', function() { + it('has the world locationSet pre-resolved', function() { + var result1 = locationManager.locationsAt([-108.557, 39.065]); // Grand Junction + expect(result1).to.be.an('object').that.has.all.keys('+[Q2]'); + var result2 = locationManager.locationsAt([-74.481, 40.797]); // Morristown + expect(result2).to.be.an('object').that.has.all.keys('+[Q2]'); + var result3 = locationManager.locationsAt([13.575, 41.207,]); // Gaeta + expect(result3).to.be.an('object').that.has.all.keys('+[Q2]'); + }); + + it('returns valid locations at a given lon,lat', function(done) { + // setup, load colorado.geojson and resolve some locationSets + locationManager.mergeCustomGeoJSON(fc); + locationManager.mergeLocationSets([ + { id: 'OSM-World', locationSet: { include: ['001'] } }, + { id: 'OSM-USA', locationSet: { include: ['us'] } }, + { id: 'OSM-Colorado', locationSet: { include: ['colorado.geojson'] } } + ]) + .then(function() { + var result1 = locationManager.locationsAt([-108.557, 39.065]); // Grand Junction + expect(result1).to.be.an('object').that.has.all.keys('+[Q2]', '+[Q30]', '+[colorado.geojson]'); + var result2 = locationManager.locationsAt([-74.481, 40.797]); // Morristown + expect(result2).to.be.an('object').that.has.all.keys('+[Q2]', '+[Q30]'); + var result3 = locationManager.locationsAt([13.575, 41.207,]); // Gaeta + expect(result3).to.be.an('object').that.has.all.keys('+[Q2]'); + }) + .finally(done); + + window.setTimeout(function() {}, 20); // async - to let the promise settle in phantomjs + }); + }); + +});