├── src ├── partials │ ├── geostore-tail.js │ └── geostore-head.js ├── helpers │ ├── node │ │ └── stream.js │ ├── browser │ │ ├── stream.js │ │ └── eventemitter.js │ └── sync.js ├── memory.js └── geostore.js ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── package.json ├── Gruntfile.js ├── README.md ├── browser ├── terraformer-geostore.min.js └── terraformer-geostore.js ├── node └── terraformer-geostore.js └── spec └── geostoreSpec.js /src/partials/geostore-tail.js: -------------------------------------------------------------------------------- 1 | })); 2 | -------------------------------------------------------------------------------- /src/helpers/node/stream.js: -------------------------------------------------------------------------------- 1 | var Stream = require('stream'); 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .grunt 3 | coverage 4 | SpecRunner.html -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | sudo: false 5 | cache: 6 | directories: 7 | - node_modules -------------------------------------------------------------------------------- /src/partials/geostore-head.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | 3 | // Node. 4 | if(typeof module === 'object' && typeof module.exports === 'object') { 5 | exports = module.exports = factory(require("terraformer")); 6 | } 7 | 8 | // Browser Global. 9 | if(typeof root.navigator === "object") { 10 | if (!root.Terraformer){ 11 | throw new Error("Terraformer.GeoStore requires the core Terraformer library. http://github.com/esri/terraformer") 12 | } 13 | root.Terraformer.GeoStore = factory(root.Terraformer).GeoStore; 14 | } 15 | 16 | }(this, function(Terraformer) { 17 | -------------------------------------------------------------------------------- /src/helpers/browser/stream.js: -------------------------------------------------------------------------------- 1 | function Stream () { 2 | var self = this; 3 | 4 | EventEmitter.call(this); 5 | 6 | this._destination = [ ]; 7 | this._emit = this.emit; 8 | 9 | this.emit = function (signal, data) { 10 | var i; 11 | 12 | if (signal === "data" || signal === "end") { 13 | for (i = self._destination.length; i--;) { 14 | self._destination[i].write(data); 15 | } 16 | } 17 | self._emit(signal, data); 18 | }; 19 | } 20 | 21 | Stream.prototype.pipe = function (destination) { 22 | this._destination.push(destination); 23 | }; 24 | 25 | Stream.prototype.unpipe = function (destination) { 26 | if (!destination) { 27 | this._destination = [ ]; 28 | } else { 29 | for(var i = this._destination.length-1; i--;) { 30 | if (this._destination[i].listener === destination) { 31 | this._destination.splice(i, 1); 32 | } 33 | } 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [Unreleased] 7 | 8 | ## [1.0.5] 9 | 10 | ### Fixed 11 | 12 | * bug in `within()` 13 | 14 | ### Added 15 | 16 | * `intersects()` method 17 | * CHANGELOG 18 | 19 | ### Changed 20 | 21 | * refactor to make codebase more DRY 22 | 23 | ### Removed 24 | 25 | * s3 CDN hosting 26 | 27 | ## [1.0.4] 28 | 29 | ## [1.0.3] 30 | 31 | ## [1.0.2] 32 | 33 | ## [1.0.1] 34 | 35 | [Unreleased]: https://github.com/Esri/terraformer-geostore/compare/v1.0.5...HEAD 36 | [1.0.5]: https://github.com/Esri/terraformer-geostore/compare/v1.0.4...v1.0.5 37 | [1.0.4]: https://github.com/Esri/terraformer-geostore/compare/v1.0.3...v1.0.4 38 | [1.0.3]: https://github.com/Esri/terraformer-geostore/compare/v1.0.2...v1.0.3 39 | [1.0.2]: https://github.com/Esri/terraformer-geostore/compare/v1.0.1...v1.0.2 40 | [1.0.1]: https://github.com/Esri/terraformer-geostore/compare/v1.0.0...v1.0.1 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Esri, Inc 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "terraformer-geostore", 3 | "version": "1.0.5", 4 | "description": "GeoStore for Terraformer, helps a geo-database toolkit", 5 | "main": "node/terraformer-geostore.js", 6 | "browser": "browser/terraformer-geostore.js", 7 | "files": [ 8 | "browser/terraformer-geostore.js", 9 | "browser/terraformer-geostore.min.js" 10 | ], 11 | "scripts": { 12 | "test": "grunt test" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git@github.com:Esri/terraformer-geostore.git" 17 | }, 18 | "keywords": [ 19 | "GIS", 20 | "GeoStore", 21 | "Terraformer", 22 | "Database" 23 | ], 24 | "author": "Jerry Sievert", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/Esri/terraformer-geostore/issues" 28 | }, 29 | "dependencies": { 30 | "terraformer": "^1.0.0" 31 | }, 32 | "devDependencies": { 33 | "grunt": "^1.0.2", 34 | "grunt-contrib-concat": "^1.0.1", 35 | "grunt-contrib-jasmine": "^1.1.0", 36 | "grunt-contrib-uglify": "^3.3.0", 37 | "grunt-jasmine-node": "^0.3.1", 38 | "grunt-jasmine-nodejs": "^1.6.0", 39 | "grunt-template-jasmine-istanbul": "^0.5.0", 40 | "phantomjs-prebuilt": "^2.1.16", 41 | "terraformer-geostore-index-btree": "^1.0.0", 42 | "terraformer-rtree": "^1.0.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/helpers/sync.js: -------------------------------------------------------------------------------- 1 | // super lightweight async to sync handling 2 | 3 | function Sync () { 4 | this._steps = [ ]; 5 | this._arguments = [ ]; 6 | this._current = 0; 7 | this._error = null; 8 | } 9 | 10 | Sync.prototype.next = function () { 11 | var args = Array.prototype.slice.call(arguments); 12 | this._steps.push(args.shift()); 13 | this._arguments[this._steps.length - 1] = args; 14 | 15 | return this; 16 | }; 17 | 18 | Sync.prototype.error = function (error) { 19 | this._error = error; 20 | 21 | return this; 22 | }; 23 | 24 | Sync.prototype.done = function (err) { 25 | this._current++; 26 | var args = Array.prototype.slice.call(arguments); 27 | 28 | // if there is an error, we are done 29 | if (err) { 30 | if (this._error) { 31 | this._error.apply(this, args); 32 | } 33 | } else { 34 | if (this._steps.length) { 35 | var next = this._steps.shift(); 36 | var a = this._arguments[this._current]; 37 | var self = this; 38 | 39 | function cb (err, data) { 40 | self.done(err, data); 41 | }; 42 | a.push(cb); 43 | next.apply(this, a); 44 | } else { 45 | if (this._callback) { 46 | this._callback.apply(); 47 | } 48 | } 49 | } 50 | }; 51 | 52 | Sync.prototype.start = function (callback) { 53 | this._callback = callback; 54 | 55 | var start = this._steps.shift(), 56 | self = this; 57 | 58 | if (start) { 59 | var args = this._arguments[0]; 60 | function cb (err, data) { 61 | self.done(err, data); 62 | }; 63 | args.push(cb); 64 | start.apply(this, args); 65 | } else { 66 | if (this._callback) { 67 | this._callback(); 68 | } 69 | } 70 | }; 71 | 72 | -------------------------------------------------------------------------------- /src/helpers/browser/eventemitter.js: -------------------------------------------------------------------------------- 1 | // super light weight EventEmitter implementation 2 | 3 | function EventEmitter() { 4 | this._events = { }; 5 | this._once = { }; 6 | // default to 10 max liseners 7 | this._maxListeners = 10; 8 | 9 | this._add = function (event, listener, once) { 10 | var entry = { listener: listener }; 11 | if (once) { 12 | entry.once = true; 13 | } 14 | 15 | if (this._events[event]) { 16 | this._events[event].push(entry); 17 | } else { 18 | this._events[event] = [ entry ]; 19 | } 20 | 21 | if (this._maxListeners && this._events[event].count > this._maxListeners && console && console.warn) { 22 | console.warn("EventEmitter Error: Maximum number of listeners"); 23 | } 24 | 25 | return this; 26 | }; 27 | 28 | this.on = function (event, listener) { 29 | return this._add(event, listener); 30 | }; 31 | 32 | this.addListener = this.on; 33 | 34 | this.once = function (event, listener) { 35 | return this._add(event, listener, true); 36 | }; 37 | 38 | this.removeListener = function (event, listener) { 39 | if (!this._events[event]) { 40 | return this; 41 | } 42 | 43 | for(var i = this._events.length-1; i--;) { 44 | if (this._events[i].listener === callback) { 45 | this._events.splice(i, 1); 46 | } 47 | } 48 | 49 | return this; 50 | }; 51 | 52 | this.removeAllListeners = function (event) { 53 | this._events[event] = undefined; 54 | 55 | return this; 56 | }; 57 | 58 | this.setMaxListeners = function (count) { 59 | this._maxListeners = count; 60 | 61 | return this; 62 | }; 63 | 64 | this.emit = function () { 65 | var args = Array.prototype.slice.apply(arguments); 66 | var remove = [ ], i; 67 | 68 | if (args.length) { 69 | var event = args.shift(); 70 | 71 | if (this._events[event]) { 72 | for (i = this._events[event].length; i--;) { 73 | this._events[event][i].listener.apply(null, args); 74 | if (this._events[event][i].once) { 75 | remove.push(listener); 76 | } 77 | } 78 | } 79 | 80 | for (i = remove.length; i--;) { 81 | this.removeListener(event, remove[i]); 82 | } 83 | } 84 | 85 | return this; 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /src/memory.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | // Node. 3 | if(typeof module === 'object' && typeof module.exports === 'object') { 4 | exports = module.exports = factory(); 5 | } 6 | 7 | // AMD. 8 | if(typeof define === 'function' && define.amd) { 9 | define([], factory); 10 | } 11 | 12 | // Browser Global. 13 | if(typeof root.navigator === "object") { 14 | if (typeof root.Terraformer === "undefined"){ 15 | root.Terraformer = { }; 16 | } 17 | if (typeof root.Terraformer.Store === "undefined"){ 18 | root.Terraformer.Store = {}; 19 | } 20 | root.Terraformer.Store.Memory = factory().Memory; 21 | } 22 | }(this, function() { 23 | var exports = { }; 24 | 25 | var Terraformer; 26 | 27 | // Local Reference To Browser Global 28 | if(typeof this.navigator === "object") { 29 | Terraformer = this.Terraformer; 30 | } 31 | 32 | // Setup Node Dependencies 33 | if(typeof module === 'object' && typeof module.exports === 'object') { 34 | Terraformer = require('terraformer'); 35 | } 36 | 37 | // Setup AMD Dependencies 38 | if(arguments[0] && typeof define === 'function' && define.amd) { 39 | Terraformer = arguments[0]; 40 | } 41 | 42 | // These methods get called in context of the geostore 43 | function Memory(){ 44 | this.data = {}; 45 | } 46 | 47 | // store the data at id returns true if stored successfully 48 | Memory.prototype.add = function(geojson, callback){ 49 | if(geojson.type === "FeatureCollection"){ 50 | for (var i = 0; i < geojson.features.length; i++) { 51 | this.data[geojson.features[i].id] = geojson.features[i]; 52 | } 53 | } else { 54 | this.data[geojson.id] = geojson; 55 | } 56 | if ( callback ) callback( null, geojson); 57 | }; 58 | 59 | // remove the data from the index and data with id returns true if removed successfully. 60 | Memory.prototype.remove = function(id, callback){ 61 | delete this.data[id]; 62 | if ( callback ) { 63 | callback( null, id ); 64 | } 65 | }; 66 | 67 | // return the data stored at id 68 | Memory.prototype.get = function(id, callback){ 69 | if ( callback ) { 70 | callback( null, this.data[id] ); 71 | } 72 | }; 73 | 74 | Memory.prototype.update = function(geojson, callback){ 75 | this.data[geojson.id] = geojson; 76 | if ( callback ) { 77 | callback( null ); 78 | } 79 | }; 80 | 81 | Memory.prototype.serialize = function(callback){ 82 | var data = JSON.stringify(this.data); 83 | if ( callback ) { 84 | callback( null, data ); 85 | } 86 | }; 87 | 88 | Memory.prototype.deserialize = function(serializedStore){ 89 | this.data = JSON.parse(serializedStore); 90 | return this; 91 | }; 92 | 93 | exports.Memory = Memory; 94 | 95 | return exports; 96 | })); 97 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | module.exports = function (grunt) { 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | 7 | meta: { 8 | banner: '/*! Terraformer GeoStore - <%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n' + 9 | '* https://github.com/esri/terraformer-geostore\n' + 10 | '* Copyright (c) <%= grunt.template.today("yyyy") %> Esri, Inc.\n' + 11 | '* Licensed MIT */' 12 | }, 13 | 14 | uglify: { 15 | options: { 16 | report: 'gzip', 17 | banner: '<%= meta.banner %>' 18 | }, 19 | geostore: { 20 | src: ['browser/terraformer-geostore.js'], 21 | dest: 'browser/terraformer-geostore.min.js' 22 | }, 23 | versioned: { 24 | src: ['browser/terraformer-geostore.js'], 25 | dest: 'versions/terraformer-geostore-<%= pkg.version %>.min.js' 26 | } 27 | }, 28 | 29 | concat: { 30 | geostore: { 31 | src: ['', 'src/partials/geostore-head.js', 'src/helpers/sync.js', 'src/helpers/browser/eventemitter.js', 'src/helpers/browser/stream.js', 'src/geostore.js', 'src/partials/geostore-tail.js' ], 32 | dest: 'browser/terraformer-geostore.js' 33 | }, 34 | geostore_node: { 35 | src: [ 'src/partials/geostore-head.js', 'src/helpers/sync.js', 'src/helpers/node/stream.js', 'src/geostore.js', 'src/partials/geostore-tail.js' ], 36 | dest: 'node/terraformer-geostore.js' 37 | } 38 | }, 39 | 40 | jasmine: { 41 | coverage: { 42 | src: [ 43 | "browser/terraformer-geostore.js" 44 | ], 45 | options: { 46 | specs: 'spec/*Spec.js', 47 | helpers: [ 48 | "./node_modules/terraformer/terraformer.js", 49 | "./node_modules/terraformer-rtree/terraformer-geostore-rtree.js", 50 | "./src/memory.js" 51 | ], 52 | //keepRunner: true, 53 | outfile: 'SpecRunner.html', 54 | template: require('grunt-template-jasmine-istanbul'), 55 | templateOptions: { 56 | coverage: './coverage/coverage.json', 57 | report: './coverage', 58 | thresholds: { 59 | lines: 60, 60 | statements: 60, 61 | branches: 40, 62 | functions: 60 63 | } 64 | } 65 | } 66 | } 67 | }, 68 | 69 | // lets use jasmine 2.0! 70 | jasmine_nodejs: { 71 | // task specific (default) options 72 | options: { 73 | useHelpers: true, 74 | // global helpers, available to all task targets. accepts globs.. 75 | helpers: [], 76 | random: false, 77 | seed: null, 78 | defaultTimeout: null, // defaults to 5000 79 | stopOnFailure: false, 80 | traceFatal: true, 81 | // configure one or more built-in reporters 82 | reporters: { 83 | console: { 84 | colors: true, // (0|false)|(1|true)|2 85 | cleanStack: 1, // (0|false)|(1|true)|2|3 86 | verbosity: 4, // (0|false)|1|2|3|(4|true) 87 | listStyle: "indent", // "flat"|"indent" 88 | activity: false 89 | } 90 | }, 91 | // add custom Jasmine reporter(s) 92 | customReporters: [] 93 | }, 94 | your_target: { 95 | // spec files 96 | specs: [ 97 | "./spec/**" 98 | ] 99 | } 100 | } 101 | }); 102 | 103 | var awsExists = fs.existsSync(process.env.HOME + '/terraformer-s3.json'); 104 | 105 | if (awsExists) { 106 | grunt.config.set('aws', grunt.file.readJSON(process.env.HOME + '/terraformer-s3.json')); 107 | } 108 | 109 | grunt.loadNpmTasks('grunt-contrib-uglify'); 110 | grunt.loadNpmTasks('grunt-contrib-concat'); 111 | grunt.loadNpmTasks('grunt-contrib-jasmine'); 112 | grunt.loadNpmTasks('grunt-jasmine-nodejs'); 113 | 114 | grunt.registerTask('test', [ 'concat', 'jasmine', 'jasmine_nodejs' ]); 115 | grunt.registerTask('default', [ 'test' ]); 116 | grunt.registerTask('version', [ 'test', 'uglify' ]); 117 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Important! 2 | 3 | This repo is part of the Terraformer project which has been archived. See https://github.com/Esri/terraformer#important for more details. 4 | 5 | [![npm][npm-image]][npm-url] 6 | [![travis][travis-image]][travis-url] 7 | 8 | [npm-image]: https://img.shields.io/npm/v/terraformer-geostore.svg?style=flat-square 9 | [npm-url]: https://www.npmjs.com/package/terraformer-geostore 10 | [travis-image]: https://img.shields.io/travis/Esri/terraformer-geostore/master.svg?style=flat-square 11 | [travis-url]: https://travis-ci.org/Esri/terraformer-geostore 12 | 13 | # terraformer-geostore 14 | 15 | `Terraformer.GeoStore` is a class built for handing lightweight storage and querying large numbers of [GeoJSON Features](http://www.geojson.org/geojson-spec.html#feature-objects). It is very fast and can index the [rough US counties data](https://github.com/Esri/Terraformer/blob/master/examples/geostore/counties_rough.json) (~950k features) in about 120ms and do a point in polygon search contains a point in ~6.5ms. 16 | 17 | ## Installing 18 | 19 | ### Node.js 20 | 21 | ```bash 22 | $ npm install terraformer 23 | $ npm install terraformer-geostore 24 | $ npm install terraformer-rtree 25 | $ npm install terraformer-geostore-memory 26 | ``` 27 | 28 | ### Browser 29 | 30 | In the browser, [Terraformer](http://github.com/esri/terraformer) is required. 31 | 32 | You can use [Bower](http://bower.io/) to install the components if you like or download them and host them yourself. 33 | 34 | ```html 35 | 36 | 37 | 38 | 39 | ``` 40 | 41 | ## Documentation 42 | 43 | For a full guide to GeoStore check out the [Terraformer website](https://github.com/Esri/terraformer/blob/master/docs/geostore.md). 44 | 45 | ```js 46 | var store = new Terraformer.GeoStore({ 47 | store: new Terraformer.GeoStore.Memory(), 48 | index: new Terraformer.RTree() 49 | }); 50 | 51 | // Add a GeoJSON feature 52 | store.add({ 53 | "type": "Feature", 54 | "properties": { 55 | "name": "Ladds Addition" 56 | }, 57 | "id": "ladds-addition", 58 | "geometry": { 59 | "type": "Polygon", 60 | "coordinates": [ 61 | [ [ -122.65355587005614, 45.50499344809821 ], [ -122.65355587005614, 45.512061121601 ], [ -122.64535903930664, 45.512061121601 ], [ -122.64535903930664, 45.50499344809821 ], [ -122.65355587005614, 45.50499344809821 ] ] 62 | ] 63 | } 64 | }, function (err, success) { 65 | // callback when the geojson is added 66 | }); 67 | 68 | // You can also add a FeatureCollection 69 | store.add({ 70 | "type": "FeatureCollection", 71 | "features": [ 72 | { 73 | "type": "Feature", 74 | "properties": { 75 | "name": "Esri PDX" 76 | }, 77 | "id": "esri-pdx", 78 | "geometry": { 79 | "type": "Point", 80 | "coordinates": [ -122.67629563808441, 45.51646293140592 ] 81 | } 82 | }, 83 | { 84 | "type": "Feature", 85 | "properties": { 86 | "name": "Barista" 87 | }, 88 | "id": "barista", 89 | "geometry": { 90 | "type": "Point", 91 | "coordinates": [ -122.67520129680635, 45.51926322043975 ] 92 | } 93 | } 94 | ] 95 | }, function (err, success) { 96 | // callback when all features are added 97 | }); 98 | 99 | // ask the store which features are within a given polygon. 100 | store.within({ 101 | "type": "Polygon", 102 | "coordinates": [ 103 | [ [-122.69290924072266, 45.54038305764738], [-122.72054672241211, 45.535453299886896], [-122.69479751586914, 45.51464736754301], [-122.67848968505858, 45.495398037299395], [-122.66836166381836, 45.495398037299395], [-122.66681671142577, 45.50321887154943], [-122.67127990722655, 45.51067773196122], [-122.67127990722655, 45.522585798722176], [-122.67110824584961, 45.53028260179986], [-122.69290924072266, 45.54038305764738] ] 104 | ] 105 | }, function (err, results) { 106 | for (var i = results.length - 1; i >= 0; i--) { 107 | console.log(results[i].id); 108 | }; 109 | }); 110 | ``` 111 | 112 | ## Issues 113 | 114 | Find a bug or want to request a new feature? Please let us know by submitting an issue. 115 | 116 | ## Contributing 117 | 118 | Esri welcomes contributions from anyone and everyone. Please see our [guidelines for contributing](https://github.com/Esri/contributing). 119 | 120 | ## Licensing 121 | 122 | A copy of the license is available in the repository's [LICENSE](./LICENSE) file. 123 | -------------------------------------------------------------------------------- /browser/terraformer-geostore.min.js: -------------------------------------------------------------------------------- 1 | /*! Terraformer GeoStore - 1.0.2 - 2013-12-04 2 | * https://github.com/esri/terraformer-geostore 3 | * Copyright (c) 2013 Esri, Inc. 4 | * Licensed MIT */!function(a,b){if("object"==typeof module&&"object"==typeof module.exports&&(exports=module.exports=b(require("terraformer"))),"object"==typeof a.navigator){if(!a.Terraformer)throw new Error("Terraformer.GeoStore requires the core Terraformer library. http://github.com/esri/terraformer");a.Terraformer.GeoStore=b(a.Terraformer).GeoStore}}(this,function(a){function b(){this._steps=[],this._arguments=[],this._current=0,this._error=null}function c(){this._events={},this._once={},this._maxListeners=10,this._add=function(a,b,c){var d={listener:b};return c&&(d.once=!0),this._events[a]?this._events[a].push(d):this._events[a]=[d],this._maxListeners&&this._events[a].count>this._maxListeners&&console&&console.warn&&console.warn("EventEmitter Error: Maximum number of listeners"),this},this.on=function(a,b){return this._add(a,b)},this.addListener=this.on,this.once=function(a,b){return this._add(a,b,!0)},this.removeListener=function(a){if(!this._events[a])return this;for(var b=this._events.length-1;b--;)this._events[b].listener===callback&&this._events.splice(b,1);return this},this.removeAllListeners=function(a){return this._events[a]=void 0,this},this.setMaxListeners=function(a){return this._maxListeners=a,this},this.emit=function(){var a,b=Array.prototype.slice.apply(arguments),c=[];if(b.length){var d=b.shift();if(this._events[d])for(a=this._events[d].length;a--;)this._events[d][a].listener.apply(null,b),this._events[d][a].once&&c.push(listener);for(a=c.length;a--;)this.removeListener(d,c[a])}return this}}function d(){var a=this;c.call(this),this._destination=[],this._emit=this.emit,this.emit=function(b,c){var d;if("data"===b)for(d=a._destination.length;d--;)a._destination[d].write(c);else if("end"===b)for(d=a._destination.length;d--;)a._destination[d].write(c);a._emit(b,c)}}function e(a,b){var c=arguments.length>2?Array.prototype.slice.call(arguments,2):null;return function(){return b.apply(a,c||arguments)}}function f(a){if(!a.store||!a.index)throw new Error("Terraformer.GeoStore requires an instace of a Terraformer.Store and a instance of Terraformer.RTree");this.index=a.index,this.store=a.store,this._stream=null,this._additional_indexes=[]}function g(a,b,c){var d=a.property;b.properties&&void 0!==b.properties[d]&&null!==b.properties[d]&&a.index.add(b.properties[d],b.id,c)}function h(a,b,c){var d=a.property;b.properties&&void 0!==b.properties[d]&&null!==b.properties[d]&&a.index.remove(b.properties[d],b.id,c)}function i(a,b,c,d){for(var e=Object.keys(b),f=0,g=0;g=d.length&&(m?f&&f("Could not get all geometries",null):n._stream?n._stream=null:f&&f(null,k))}},c=function(){l++,m++,l>=d.length&&f&&f("Could not get all geometries",null)};if(d&&d.length)for(var g=function(a,d){a?c():b(d)},i=0;i=d.length&&(m?f&&f("Could not get all geometries",null):n._stream?n._stream=null:f&&f(null,k))}};o.start();var c=function(){l++,m++,l>=d.length&&f&&f("Could not get all geometries",null)};if(d&&d.length)for(var g=function(a,d){a?c():b(d)},i=0;i 2 ? Array.prototype.slice.call(arguments, 2) : null; 6 | return function () { 7 | return fn.apply(obj, args || arguments); 8 | }; 9 | } 10 | 11 | // The store object that ties everything together... 12 | /* OPTIONS 13 | { 14 | store: Terraformer.Store.Memory, 15 | index: Terraformer.RTree 16 | } 17 | */ 18 | function GeoStore(config){ 19 | 20 | if(!config.store || !config.index){ 21 | throw new Error("Terraformer.GeoStore requires an instace of a Terraformer.Store and a instance of Terraformer.RTree"); 22 | } 23 | this.index = config.index; 24 | this.store = config.store; 25 | this._stream = null; 26 | 27 | this._additional_indexes = [ ]; 28 | } 29 | 30 | function checkIndexAndAdd(index, geojson, callback) { 31 | var property = index.property; 32 | 33 | // we do not currently index NULL, might be a TODO in the future 34 | if (geojson.properties && geojson.properties[property] !== undefined && geojson.properties[property] !== null) { 35 | index.index.add(geojson.properties[property], geojson.id, callback); 36 | } 37 | } 38 | 39 | function checkIndexAndRemove(index, geojson, callback) { 40 | var property = index.property; 41 | 42 | // we do not currently index NULL, might be a TODO in the future 43 | if (geojson.properties && geojson.properties[property] !== undefined && geojson.properties[property] !== null) { 44 | index.index.remove(geojson.properties[property], geojson.id, callback); 45 | } 46 | } 47 | 48 | // add the geojson object to the store 49 | // calculate the envelope and add it to the rtree 50 | // should return a deferred 51 | GeoStore.prototype.add = function(geojson, callback){ 52 | 53 | var sync, finished, i, self = this; 54 | var addFeature = function(feature) { 55 | var bbox = Terraformer.Tools.calculateBounds(feature); 56 | if(!feature.id) { 57 | throw new Error("Terraform.GeoStore : Feature does not have an id property"); 58 | } 59 | // TODO: if we want to be atomic and consistent, this should be truly synchronous 60 | self.index.insert({ 61 | x: bbox[0], 62 | y: bbox[1], 63 | w: Math.abs(bbox[0] - bbox[2]), 64 | h: Math.abs(bbox[1] - bbox[3]) 65 | }, feature.id); 66 | }; 67 | var syncIndexes = function(feature) { 68 | for (var j = 0; j < self._additional_indexes.length; j++) { 69 | sync.next(checkIndexAndAdd, self._additional_indexes[j], feature); 70 | } 71 | } 72 | 73 | if (!geojson.type.match(/Feature/)) { 74 | throw new Error("Terraform.GeoStore : only Features and FeatureCollections are supported"); 75 | } 76 | 77 | if (this._additional_indexes && this._additional_indexes.length) { 78 | sync = new Sync(); 79 | } 80 | 81 | // set a bounding box 82 | if(geojson.type === "FeatureCollection"){ 83 | for (i = 0; i < geojson.features.length; i++) { 84 | addFeature(geojson.features[i]); 85 | } 86 | } else { 87 | addFeature(geojson); 88 | } 89 | if (sync) { 90 | if(geojson.type === "FeatureCollection"){ 91 | for (i = 0; i < geojson.features.length; i++) { 92 | syncIndexes(geojson.features[i]); 93 | } 94 | finished = function (geo) { 95 | self.store.add(geo); 96 | }; 97 | } else { 98 | syncIndexes(geojson); 99 | finished = function (geo, cb) { 100 | self.store.add(geo, cb); 101 | }; 102 | } 103 | sync.next(finished, geojson); 104 | sync.start(callback); 105 | } else { 106 | this.store.add(geojson, callback); 107 | } 108 | }; 109 | 110 | GeoStore.prototype.remove = function(id, callback){ 111 | var self = this; 112 | this.get(id, bind(this, function(error, geojson){ 113 | if ( error ){ 114 | callback("Could not get feature to remove", null); 115 | } else { 116 | this.index.remove(geojson, id, bind(this, function(error, leaf){ 117 | if(error){ 118 | callback("Could not remove from index", null); 119 | } else { 120 | // check for additional indexes and add to them if there are property matches 121 | if (this._additional_indexes && this._additional_indexes.length) { 122 | sync = new Sync(); 123 | 124 | for (j = 0; j < this._additional_indexes.length; j++) { 125 | sync.next(checkIndexAndRemove, this._additional_indexes[j], geojson); 126 | } 127 | 128 | var finished = function (geo, cb) { 129 | self.store.remove(geo, cb); 130 | }; 131 | 132 | sync.next(finished, geojson); 133 | sync.start(callback); 134 | } else { 135 | this.store.remove(id, callback); 136 | } 137 | } 138 | })); 139 | } 140 | })); 141 | }; 142 | 143 | GeoStore.prototype._genericSpatialOperation = function (geojson, indexQuery, operationName, callback) { 144 | var indexSearchMethod, shapeGeometryTest; 145 | switch (operationName) { 146 | case 'within': 147 | indexSearchMethod = this.index.within; 148 | shapeGeometryTest = function(geometry, shape) { 149 | return geometry.within(shape); 150 | }; 151 | break; 152 | case 'contains': 153 | indexSearchMethod = this.index.search; 154 | shapeGeometryTest = function(geometry, shape) { 155 | return shape.within(geometry); 156 | }; 157 | break; 158 | case 'intersects': 159 | indexSearchMethod = this.index.search; 160 | shapeGeometryTest = function(geometry, shape) { 161 | return shape.intersects(geometry); 162 | }; 163 | break; 164 | } 165 | 166 | // make a new deferred 167 | var shape = new Terraformer.Primitive(geojson); 168 | 169 | // create our envelope 170 | var envelope = Terraformer.Tools.calculateEnvelope(shape); 171 | 172 | // search the index 173 | indexSearchMethod(envelope, bind(this, function(err, found){ 174 | var results = []; 175 | var completed = 0; 176 | var errors = 0; 177 | var self = this; 178 | var sync = new Sync(); 179 | var set; 180 | var i; 181 | 182 | // should we do set elimination with additional indexes? 183 | if (indexQuery && self._additional_indexes.length) { 184 | // convert "found" to an object with keys 185 | set = { }; 186 | 187 | for (i = 0; i < found.length; i++) { 188 | set[found[i]] = true; 189 | } 190 | 191 | // iterate through the queries, find the correct indexes, and apply them 192 | var keys = Object.keys(indexQuery); 193 | 194 | for (var j = 0; j < keys.length; j++) { 195 | for (i = 0; i < self._additional_indexes.length; i++) { 196 | // index property matches query 197 | if (self._additional_indexes[i].property === keys[j]) { 198 | var which = indexQuery[keys[j]], index = self._additional_indexes[i].index; 199 | 200 | sync.next(function (index, which, set, cb) { 201 | var next = this; 202 | eliminateForIndex(index, which, set, function (err, newSet) { 203 | set = newSet; 204 | cb(err); 205 | }); 206 | }, index, which, set); 207 | } 208 | } 209 | } 210 | } 211 | 212 | sync.next(function () { 213 | // if we have a set, it is our new "found" 214 | if (set) { 215 | found = Object.keys(set); 216 | } 217 | 218 | // the function to evalute results from the index 219 | var evaluate = function(primitive){ 220 | completed++; 221 | if ( primitive ){ 222 | var geometry = new Terraformer.Primitive(primitive.geometry); 223 | if (shapeGeometryTest(geometry, shape)){ 224 | if (self._stream) { 225 | if (completed === found.length) { 226 | if (operationName == "within") { 227 | self._stream.emit("done", primitive); 228 | } else { 229 | self._stream.emit("data", primitive); 230 | } 231 | self._stream.emit("end"); 232 | } else { 233 | self._stream.emit("data", primitive); 234 | } 235 | } else { 236 | results.push(primitive); 237 | } 238 | } 239 | 240 | if(completed >= found.length){ 241 | if(!errors){ 242 | if (self._stream) { 243 | self._stream = null; 244 | } else if (callback) { 245 | callback( null, results ); 246 | } 247 | } else { 248 | if (callback) { 249 | callback("Could not get all geometries", null); 250 | } 251 | } 252 | } 253 | } 254 | }; 255 | 256 | var error = function(){ 257 | completed++; 258 | errors++; 259 | if(completed >= found.length){ 260 | if (callback) { 261 | callback("Could not get all geometries", null); 262 | } 263 | } 264 | }; 265 | 266 | // for each result see if the polygon contains the point 267 | if(found && found.length){ 268 | var getCB = function(err, result){ 269 | if (err) { 270 | error(); 271 | } else { 272 | evaluate( result ); 273 | } 274 | }; 275 | 276 | for (var i = 0; i < found.length; i++) { 277 | self.get(found[i], getCB); 278 | } 279 | } else { 280 | if (callback) { 281 | callback(null, results); 282 | } 283 | } 284 | }); 285 | 286 | sync.start(); 287 | 288 | })); 289 | }; 290 | 291 | GeoStore.prototype.intersects = function (geojson) { 292 | var indexQuery; 293 | var args = Array.prototype.slice.call(arguments); 294 | args.shift(); 295 | 296 | var callback = args.pop(); 297 | if (args.length) { 298 | indexQuery = args[0]; 299 | } 300 | 301 | return this._genericSpatialOperation(geojson, indexQuery, 'intersects', callback); 302 | }; 303 | 304 | GeoStore.prototype.contains = function (geojson) { 305 | var indexQuery; 306 | var args = Array.prototype.slice.call(arguments); 307 | args.shift(); 308 | 309 | var callback = args.pop(); 310 | if (args.length) { 311 | indexQuery = args[0]; 312 | } 313 | 314 | return this._genericSpatialOperation(geojson, indexQuery, 'contains', callback); 315 | }; 316 | 317 | GeoStore.prototype.within = function(geojson){ 318 | var indexQuery; 319 | var args = Array.prototype.slice.call(arguments); 320 | args.shift(); 321 | var callback = args.pop(); 322 | if (args.length) { 323 | indexQuery = args[0]; 324 | } 325 | 326 | return this._genericSpatialOperation(geojson, indexQuery, 'within', callback); 327 | }; 328 | 329 | GeoStore.prototype.update = function(geojson, callback){ 330 | var feature = Terraformer.Primitive(geojson); 331 | 332 | if (feature.type !== "Feature") { 333 | throw new Error("Terraform.GeoStore : only Features and FeatureCollections are supported"); 334 | } 335 | 336 | if(!feature.id) { 337 | throw new Error("Terraform.GeoStore : Feature does not have an id property"); 338 | } 339 | 340 | this.get(feature.id, bind(this, function( error, oldFeatureGeoJSON ){ 341 | if ( error ){ 342 | callback("Could find feature", null); 343 | } else { 344 | var oldFeature = new Terraformer.Primitive(oldFeatureGeoJSON); 345 | this.index.remove(oldFeature.envelope(), oldFeature.id); 346 | this.index.insert(feature.envelope(), feature.id); 347 | this.store.update(feature, callback); 348 | } 349 | })); 350 | 351 | }; 352 | 353 | // gets an item by id 354 | GeoStore.prototype.get = function(id, callback){ 355 | this.store.get( id, callback ); 356 | }; 357 | 358 | GeoStore.prototype.createReadStream = function () { 359 | this._stream = new Stream(); 360 | return this._stream; 361 | }; 362 | 363 | // add an index 364 | GeoStore.prototype.addIndex = function(index) { 365 | this._additional_indexes.push(index); 366 | }; 367 | 368 | 369 | /* 370 | "crime": 371 | { 372 | "equals": "arson" 373 | } 374 | 375 | index -> specific index that references the property keyword 376 | query -> object containing the specific queries for the index 377 | set -> object containing keys of all of the id's matching currently 378 | 379 | callback -> object containing keys of all of the id's still matching: 380 | { 381 | 1: true, 382 | 23: true 383 | } 384 | 385 | TODO: add functionality for 386 | "crime": 387 | { 388 | "or": { 389 | "equals": "arson", 390 | "equals": "theft" 391 | } 392 | } 393 | */ 394 | function eliminateForIndex(index, query, set, callback) { 395 | var queryKeys = Object.keys(query); 396 | var count = 0; 397 | 398 | for (var i = 0; i < queryKeys.length; i++) { 399 | if (typeof index[queryKeys[i]] !== "function") { 400 | callback("Index does not have a method matching " + queryKeys[i]); 401 | return; 402 | } 403 | 404 | index[queryKeys[i]](query[i], function (err, data) { 405 | count++; 406 | 407 | if (err) { 408 | callback(err); 409 | 410 | // short-circuit the scan, we hit an error. this is fatal. 411 | count = queryKeys.length; 412 | return; 413 | } else { 414 | var setKeys = Object.keys(set); 415 | for (var j = 0; j < setKeys.length; j++) { 416 | if (!data[setKeys[j]]) { 417 | delete set[setKeys[j]]; 418 | } 419 | } 420 | } 421 | 422 | if (count === queryKeys.length) { 423 | callback(null, set); 424 | } 425 | }); 426 | } 427 | } 428 | 429 | exports.GeoStore = GeoStore; 430 | 431 | return exports; 432 | 433 | -------------------------------------------------------------------------------- /node/terraformer-geostore.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | 3 | // Node. 4 | if(typeof module === 'object' && typeof module.exports === 'object') { 5 | exports = module.exports = factory(require("terraformer")); 6 | } 7 | 8 | // Browser Global. 9 | if(typeof root.navigator === "object") { 10 | if (!root.Terraformer){ 11 | throw new Error("Terraformer.GeoStore requires the core Terraformer library. http://github.com/esri/terraformer") 12 | } 13 | root.Terraformer.GeoStore = factory(root.Terraformer).GeoStore; 14 | } 15 | 16 | }(this, function(Terraformer) { 17 | 18 | // super lightweight async to sync handling 19 | 20 | function Sync () { 21 | this._steps = [ ]; 22 | this._arguments = [ ]; 23 | this._current = 0; 24 | this._error = null; 25 | } 26 | 27 | Sync.prototype.next = function () { 28 | var args = Array.prototype.slice.call(arguments); 29 | this._steps.push(args.shift()); 30 | this._arguments[this._steps.length - 1] = args; 31 | 32 | return this; 33 | }; 34 | 35 | Sync.prototype.error = function (error) { 36 | this._error = error; 37 | 38 | return this; 39 | }; 40 | 41 | Sync.prototype.done = function (err) { 42 | this._current++; 43 | var args = Array.prototype.slice.call(arguments); 44 | 45 | // if there is an error, we are done 46 | if (err) { 47 | if (this._error) { 48 | this._error.apply(this, args); 49 | } 50 | } else { 51 | if (this._steps.length) { 52 | var next = this._steps.shift(); 53 | var a = this._arguments[this._current]; 54 | var self = this; 55 | 56 | function cb (err, data) { 57 | self.done(err, data); 58 | }; 59 | a.push(cb); 60 | next.apply(this, a); 61 | } else { 62 | if (this._callback) { 63 | this._callback.apply(); 64 | } 65 | } 66 | } 67 | }; 68 | 69 | Sync.prototype.start = function (callback) { 70 | this._callback = callback; 71 | 72 | var start = this._steps.shift(), 73 | self = this; 74 | 75 | if (start) { 76 | var args = this._arguments[0]; 77 | function cb (err, data) { 78 | self.done(err, data); 79 | }; 80 | args.push(cb); 81 | start.apply(this, args); 82 | } else { 83 | if (this._callback) { 84 | this._callback(); 85 | } 86 | } 87 | }; 88 | 89 | 90 | var Stream = require('stream'); 91 | 92 | 93 | var exports = { }; 94 | 95 | function bind(obj, fn) { 96 | var args = arguments.length > 2 ? Array.prototype.slice.call(arguments, 2) : null; 97 | return function () { 98 | return fn.apply(obj, args || arguments); 99 | }; 100 | } 101 | 102 | // The store object that ties everything together... 103 | /* OPTIONS 104 | { 105 | store: Terraformer.Store.Memory, 106 | index: Terraformer.RTree 107 | } 108 | */ 109 | function GeoStore(config){ 110 | 111 | if(!config.store || !config.index){ 112 | throw new Error("Terraformer.GeoStore requires an instace of a Terraformer.Store and a instance of Terraformer.RTree"); 113 | } 114 | this.index = config.index; 115 | this.store = config.store; 116 | this._stream = null; 117 | 118 | this._additional_indexes = [ ]; 119 | } 120 | 121 | function checkIndexAndAdd(index, geojson, callback) { 122 | var property = index.property; 123 | 124 | // we do not currently index NULL, might be a TODO in the future 125 | if (geojson.properties && geojson.properties[property] !== undefined && geojson.properties[property] !== null) { 126 | index.index.add(geojson.properties[property], geojson.id, callback); 127 | } 128 | } 129 | 130 | function checkIndexAndRemove(index, geojson, callback) { 131 | var property = index.property; 132 | 133 | // we do not currently index NULL, might be a TODO in the future 134 | if (geojson.properties && geojson.properties[property] !== undefined && geojson.properties[property] !== null) { 135 | index.index.remove(geojson.properties[property], geojson.id, callback); 136 | } 137 | } 138 | 139 | // add the geojson object to the store 140 | // calculate the envelope and add it to the rtree 141 | // should return a deferred 142 | GeoStore.prototype.add = function(geojson, callback){ 143 | 144 | var sync, finished, i, self = this; 145 | var addFeature = function(feature) { 146 | var bbox = Terraformer.Tools.calculateBounds(feature); 147 | if(!feature.id) { 148 | throw new Error("Terraform.GeoStore : Feature does not have an id property"); 149 | } 150 | // TODO: if we want to be atomic and consistent, this should be truly synchronous 151 | self.index.insert({ 152 | x: bbox[0], 153 | y: bbox[1], 154 | w: Math.abs(bbox[0] - bbox[2]), 155 | h: Math.abs(bbox[1] - bbox[3]) 156 | }, feature.id); 157 | }; 158 | var syncIndexes = function(feature) { 159 | for (var j = 0; j < self._additional_indexes.length; j++) { 160 | sync.next(checkIndexAndAdd, self._additional_indexes[j], feature); 161 | } 162 | } 163 | 164 | if (!geojson.type.match(/Feature/)) { 165 | throw new Error("Terraform.GeoStore : only Features and FeatureCollections are supported"); 166 | } 167 | 168 | if (this._additional_indexes && this._additional_indexes.length) { 169 | sync = new Sync(); 170 | } 171 | 172 | // set a bounding box 173 | if(geojson.type === "FeatureCollection"){ 174 | for (i = 0; i < geojson.features.length; i++) { 175 | addFeature(geojson.features[i]); 176 | } 177 | } else { 178 | addFeature(geojson); 179 | } 180 | if (sync) { 181 | if(geojson.type === "FeatureCollection"){ 182 | for (i = 0; i < geojson.features.length; i++) { 183 | syncIndexes(geojson.features[i]); 184 | } 185 | finished = function (geo) { 186 | self.store.add(geo); 187 | }; 188 | } else { 189 | syncIndexes(geojson); 190 | finished = function (geo, cb) { 191 | self.store.add(geo, cb); 192 | }; 193 | } 194 | sync.next(finished, geojson); 195 | sync.start(callback); 196 | } else { 197 | this.store.add(geojson, callback); 198 | } 199 | }; 200 | 201 | GeoStore.prototype.remove = function(id, callback){ 202 | var self = this; 203 | this.get(id, bind(this, function(error, geojson){ 204 | if ( error ){ 205 | callback("Could not get feature to remove", null); 206 | } else { 207 | this.index.remove(geojson, id, bind(this, function(error, leaf){ 208 | if(error){ 209 | callback("Could not remove from index", null); 210 | } else { 211 | // check for additional indexes and add to them if there are property matches 212 | if (this._additional_indexes && this._additional_indexes.length) { 213 | sync = new Sync(); 214 | 215 | for (j = 0; j < this._additional_indexes.length; j++) { 216 | sync.next(checkIndexAndRemove, this._additional_indexes[j], geojson); 217 | } 218 | 219 | var finished = function (geo, cb) { 220 | self.store.remove(geo, cb); 221 | }; 222 | 223 | sync.next(finished, geojson); 224 | sync.start(callback); 225 | } else { 226 | this.store.remove(id, callback); 227 | } 228 | } 229 | })); 230 | } 231 | })); 232 | }; 233 | 234 | GeoStore.prototype._genericSpatialOperation = function (geojson, indexQuery, operationName, callback) { 235 | var indexSearchMethod, shapeGeometryTest; 236 | switch (operationName) { 237 | case 'within': 238 | indexSearchMethod = this.index.within; 239 | shapeGeometryTest = function(geometry, shape) { 240 | return geometry.within(shape); 241 | }; 242 | break; 243 | case 'contains': 244 | indexSearchMethod = this.index.search; 245 | shapeGeometryTest = function(geometry, shape) { 246 | return shape.within(geometry); 247 | }; 248 | break; 249 | case 'intersects': 250 | indexSearchMethod = this.index.search; 251 | shapeGeometryTest = function(geometry, shape) { 252 | return shape.intersects(geometry); 253 | }; 254 | break; 255 | } 256 | 257 | // make a new deferred 258 | var shape = new Terraformer.Primitive(geojson); 259 | 260 | // create our envelope 261 | var envelope = Terraformer.Tools.calculateEnvelope(shape); 262 | 263 | // search the index 264 | indexSearchMethod(envelope, bind(this, function(err, found){ 265 | var results = []; 266 | var completed = 0; 267 | var errors = 0; 268 | var self = this; 269 | var sync = new Sync(); 270 | var set; 271 | var i; 272 | 273 | // should we do set elimination with additional indexes? 274 | if (indexQuery && self._additional_indexes.length) { 275 | // convert "found" to an object with keys 276 | set = { }; 277 | 278 | for (i = 0; i < found.length; i++) { 279 | set[found[i]] = true; 280 | } 281 | 282 | // iterate through the queries, find the correct indexes, and apply them 283 | var keys = Object.keys(indexQuery); 284 | 285 | for (var j = 0; j < keys.length; j++) { 286 | for (i = 0; i < self._additional_indexes.length; i++) { 287 | // index property matches query 288 | if (self._additional_indexes[i].property === keys[j]) { 289 | var which = indexQuery[keys[j]], index = self._additional_indexes[i].index; 290 | 291 | sync.next(function (index, which, set, cb) { 292 | var next = this; 293 | eliminateForIndex(index, which, set, function (err, newSet) { 294 | set = newSet; 295 | cb(err); 296 | }); 297 | }, index, which, set); 298 | } 299 | } 300 | } 301 | } 302 | 303 | sync.next(function () { 304 | // if we have a set, it is our new "found" 305 | if (set) { 306 | found = Object.keys(set); 307 | } 308 | 309 | // the function to evalute results from the index 310 | var evaluate = function(primitive){ 311 | completed++; 312 | if ( primitive ){ 313 | var geometry = new Terraformer.Primitive(primitive.geometry); 314 | if (shapeGeometryTest(geometry, shape)){ 315 | if (self._stream) { 316 | if (completed === found.length) { 317 | if (operationName == "within") { 318 | self._stream.emit("done", primitive); 319 | } else { 320 | self._stream.emit("data", primitive); 321 | } 322 | self._stream.emit("end"); 323 | } else { 324 | self._stream.emit("data", primitive); 325 | } 326 | } else { 327 | results.push(primitive); 328 | } 329 | } 330 | 331 | if(completed >= found.length){ 332 | if(!errors){ 333 | if (self._stream) { 334 | self._stream = null; 335 | } else if (callback) { 336 | callback( null, results ); 337 | } 338 | } else { 339 | if (callback) { 340 | callback("Could not get all geometries", null); 341 | } 342 | } 343 | } 344 | } 345 | }; 346 | 347 | var error = function(){ 348 | completed++; 349 | errors++; 350 | if(completed >= found.length){ 351 | if (callback) { 352 | callback("Could not get all geometries", null); 353 | } 354 | } 355 | }; 356 | 357 | // for each result see if the polygon contains the point 358 | if(found && found.length){ 359 | var getCB = function(err, result){ 360 | if (err) { 361 | error(); 362 | } else { 363 | evaluate( result ); 364 | } 365 | }; 366 | 367 | for (var i = 0; i < found.length; i++) { 368 | self.get(found[i], getCB); 369 | } 370 | } else { 371 | if (callback) { 372 | callback(null, results); 373 | } 374 | } 375 | }); 376 | 377 | sync.start(); 378 | 379 | })); 380 | }; 381 | 382 | GeoStore.prototype.intersects = function (geojson) { 383 | var indexQuery; 384 | var args = Array.prototype.slice.call(arguments); 385 | args.shift(); 386 | 387 | var callback = args.pop(); 388 | if (args.length) { 389 | indexQuery = args[0]; 390 | } 391 | 392 | return this._genericSpatialOperation(geojson, indexQuery, 'intersects', callback); 393 | }; 394 | 395 | GeoStore.prototype.contains = function (geojson) { 396 | var indexQuery; 397 | var args = Array.prototype.slice.call(arguments); 398 | args.shift(); 399 | 400 | var callback = args.pop(); 401 | if (args.length) { 402 | indexQuery = args[0]; 403 | } 404 | 405 | return this._genericSpatialOperation(geojson, indexQuery, 'contains', callback); 406 | }; 407 | 408 | GeoStore.prototype.within = function(geojson){ 409 | var indexQuery; 410 | var args = Array.prototype.slice.call(arguments); 411 | args.shift(); 412 | var callback = args.pop(); 413 | if (args.length) { 414 | indexQuery = args[0]; 415 | } 416 | 417 | return this._genericSpatialOperation(geojson, indexQuery, 'within', callback); 418 | }; 419 | 420 | GeoStore.prototype.update = function(geojson, callback){ 421 | var feature = Terraformer.Primitive(geojson); 422 | 423 | if (feature.type !== "Feature") { 424 | throw new Error("Terraform.GeoStore : only Features and FeatureCollections are supported"); 425 | } 426 | 427 | if(!feature.id) { 428 | throw new Error("Terraform.GeoStore : Feature does not have an id property"); 429 | } 430 | 431 | this.get(feature.id, bind(this, function( error, oldFeatureGeoJSON ){ 432 | if ( error ){ 433 | callback("Could find feature", null); 434 | } else { 435 | var oldFeature = new Terraformer.Primitive(oldFeatureGeoJSON); 436 | this.index.remove(oldFeature.envelope(), oldFeature.id); 437 | this.index.insert(feature.envelope(), feature.id); 438 | this.store.update(feature, callback); 439 | } 440 | })); 441 | 442 | }; 443 | 444 | // gets an item by id 445 | GeoStore.prototype.get = function(id, callback){ 446 | this.store.get( id, callback ); 447 | }; 448 | 449 | GeoStore.prototype.createReadStream = function () { 450 | this._stream = new Stream(); 451 | return this._stream; 452 | }; 453 | 454 | // add an index 455 | GeoStore.prototype.addIndex = function(index) { 456 | this._additional_indexes.push(index); 457 | }; 458 | 459 | 460 | /* 461 | "crime": 462 | { 463 | "equals": "arson" 464 | } 465 | 466 | index -> specific index that references the property keyword 467 | query -> object containing the specific queries for the index 468 | set -> object containing keys of all of the id's matching currently 469 | 470 | callback -> object containing keys of all of the id's still matching: 471 | { 472 | 1: true, 473 | 23: true 474 | } 475 | 476 | TODO: add functionality for 477 | "crime": 478 | { 479 | "or": { 480 | "equals": "arson", 481 | "equals": "theft" 482 | } 483 | } 484 | */ 485 | function eliminateForIndex(index, query, set, callback) { 486 | var queryKeys = Object.keys(query); 487 | var count = 0; 488 | 489 | for (var i = 0; i < queryKeys.length; i++) { 490 | if (typeof index[queryKeys[i]] !== "function") { 491 | callback("Index does not have a method matching " + queryKeys[i]); 492 | return; 493 | } 494 | 495 | index[queryKeys[i]](query[i], function (err, data) { 496 | count++; 497 | 498 | if (err) { 499 | callback(err); 500 | 501 | // short-circuit the scan, we hit an error. this is fatal. 502 | count = queryKeys.length; 503 | return; 504 | } else { 505 | var setKeys = Object.keys(set); 506 | for (var j = 0; j < setKeys.length; j++) { 507 | if (!data[setKeys[j]]) { 508 | delete set[setKeys[j]]; 509 | } 510 | } 511 | } 512 | 513 | if (count === queryKeys.length) { 514 | callback(null, set); 515 | } 516 | }); 517 | } 518 | } 519 | 520 | exports.GeoStore = GeoStore; 521 | 522 | return exports; 523 | 524 | 525 | })); 526 | -------------------------------------------------------------------------------- /browser/terraformer-geostore.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | 3 | // Node. 4 | if(typeof module === 'object' && typeof module.exports === 'object') { 5 | exports = module.exports = factory(require("terraformer")); 6 | } 7 | 8 | // Browser Global. 9 | if(typeof root.navigator === "object") { 10 | if (!root.Terraformer){ 11 | throw new Error("Terraformer.GeoStore requires the core Terraformer library. http://github.com/esri/terraformer") 12 | } 13 | root.Terraformer.GeoStore = factory(root.Terraformer).GeoStore; 14 | } 15 | 16 | }(this, function(Terraformer) { 17 | 18 | // super lightweight async to sync handling 19 | 20 | function Sync () { 21 | this._steps = [ ]; 22 | this._arguments = [ ]; 23 | this._current = 0; 24 | this._error = null; 25 | } 26 | 27 | Sync.prototype.next = function () { 28 | var args = Array.prototype.slice.call(arguments); 29 | this._steps.push(args.shift()); 30 | this._arguments[this._steps.length - 1] = args; 31 | 32 | return this; 33 | }; 34 | 35 | Sync.prototype.error = function (error) { 36 | this._error = error; 37 | 38 | return this; 39 | }; 40 | 41 | Sync.prototype.done = function (err) { 42 | this._current++; 43 | var args = Array.prototype.slice.call(arguments); 44 | 45 | // if there is an error, we are done 46 | if (err) { 47 | if (this._error) { 48 | this._error.apply(this, args); 49 | } 50 | } else { 51 | if (this._steps.length) { 52 | var next = this._steps.shift(); 53 | var a = this._arguments[this._current]; 54 | var self = this; 55 | 56 | function cb (err, data) { 57 | self.done(err, data); 58 | }; 59 | a.push(cb); 60 | next.apply(this, a); 61 | } else { 62 | if (this._callback) { 63 | this._callback.apply(); 64 | } 65 | } 66 | } 67 | }; 68 | 69 | Sync.prototype.start = function (callback) { 70 | this._callback = callback; 71 | 72 | var start = this._steps.shift(), 73 | self = this; 74 | 75 | if (start) { 76 | var args = this._arguments[0]; 77 | function cb (err, data) { 78 | self.done(err, data); 79 | }; 80 | args.push(cb); 81 | start.apply(this, args); 82 | } else { 83 | if (this._callback) { 84 | this._callback(); 85 | } 86 | } 87 | }; 88 | 89 | 90 | // super light weight EventEmitter implementation 91 | 92 | function EventEmitter() { 93 | this._events = { }; 94 | this._once = { }; 95 | // default to 10 max liseners 96 | this._maxListeners = 10; 97 | 98 | this._add = function (event, listener, once) { 99 | var entry = { listener: listener }; 100 | if (once) { 101 | entry.once = true; 102 | } 103 | 104 | if (this._events[event]) { 105 | this._events[event].push(entry); 106 | } else { 107 | this._events[event] = [ entry ]; 108 | } 109 | 110 | if (this._maxListeners && this._events[event].count > this._maxListeners && console && console.warn) { 111 | console.warn("EventEmitter Error: Maximum number of listeners"); 112 | } 113 | 114 | return this; 115 | }; 116 | 117 | this.on = function (event, listener) { 118 | return this._add(event, listener); 119 | }; 120 | 121 | this.addListener = this.on; 122 | 123 | this.once = function (event, listener) { 124 | return this._add(event, listener, true); 125 | }; 126 | 127 | this.removeListener = function (event, listener) { 128 | if (!this._events[event]) { 129 | return this; 130 | } 131 | 132 | for(var i = this._events.length-1; i--;) { 133 | if (this._events[i].listener === callback) { 134 | this._events.splice(i, 1); 135 | } 136 | } 137 | 138 | return this; 139 | }; 140 | 141 | this.removeAllListeners = function (event) { 142 | this._events[event] = undefined; 143 | 144 | return this; 145 | }; 146 | 147 | this.setMaxListeners = function (count) { 148 | this._maxListeners = count; 149 | 150 | return this; 151 | }; 152 | 153 | this.emit = function () { 154 | var args = Array.prototype.slice.apply(arguments); 155 | var remove = [ ], i; 156 | 157 | if (args.length) { 158 | var event = args.shift(); 159 | 160 | if (this._events[event]) { 161 | for (i = this._events[event].length; i--;) { 162 | this._events[event][i].listener.apply(null, args); 163 | if (this._events[event][i].once) { 164 | remove.push(listener); 165 | } 166 | } 167 | } 168 | 169 | for (i = remove.length; i--;) { 170 | this.removeListener(event, remove[i]); 171 | } 172 | } 173 | 174 | return this; 175 | }; 176 | } 177 | 178 | function Stream () { 179 | var self = this; 180 | 181 | EventEmitter.call(this); 182 | 183 | this._destination = [ ]; 184 | this._emit = this.emit; 185 | 186 | this.emit = function (signal, data) { 187 | var i; 188 | 189 | if (signal === "data" || signal === "end") { 190 | for (i = self._destination.length; i--;) { 191 | self._destination[i].write(data); 192 | } 193 | } 194 | self._emit(signal, data); 195 | }; 196 | } 197 | 198 | Stream.prototype.pipe = function (destination) { 199 | this._destination.push(destination); 200 | }; 201 | 202 | Stream.prototype.unpipe = function (destination) { 203 | if (!destination) { 204 | this._destination = [ ]; 205 | } else { 206 | for(var i = this._destination.length-1; i--;) { 207 | if (this._destination[i].listener === destination) { 208 | this._destination.splice(i, 1); 209 | } 210 | } 211 | } 212 | }; 213 | 214 | 215 | var exports = { }; 216 | 217 | function bind(obj, fn) { 218 | var args = arguments.length > 2 ? Array.prototype.slice.call(arguments, 2) : null; 219 | return function () { 220 | return fn.apply(obj, args || arguments); 221 | }; 222 | } 223 | 224 | // The store object that ties everything together... 225 | /* OPTIONS 226 | { 227 | store: Terraformer.Store.Memory, 228 | index: Terraformer.RTree 229 | } 230 | */ 231 | function GeoStore(config){ 232 | 233 | if(!config.store || !config.index){ 234 | throw new Error("Terraformer.GeoStore requires an instace of a Terraformer.Store and a instance of Terraformer.RTree"); 235 | } 236 | this.index = config.index; 237 | this.store = config.store; 238 | this._stream = null; 239 | 240 | this._additional_indexes = [ ]; 241 | } 242 | 243 | function checkIndexAndAdd(index, geojson, callback) { 244 | var property = index.property; 245 | 246 | // we do not currently index NULL, might be a TODO in the future 247 | if (geojson.properties && geojson.properties[property] !== undefined && geojson.properties[property] !== null) { 248 | index.index.add(geojson.properties[property], geojson.id, callback); 249 | } 250 | } 251 | 252 | function checkIndexAndRemove(index, geojson, callback) { 253 | var property = index.property; 254 | 255 | // we do not currently index NULL, might be a TODO in the future 256 | if (geojson.properties && geojson.properties[property] !== undefined && geojson.properties[property] !== null) { 257 | index.index.remove(geojson.properties[property], geojson.id, callback); 258 | } 259 | } 260 | 261 | // add the geojson object to the store 262 | // calculate the envelope and add it to the rtree 263 | // should return a deferred 264 | GeoStore.prototype.add = function(geojson, callback){ 265 | 266 | var sync, finished, i, self = this; 267 | var addFeature = function(feature) { 268 | var bbox = Terraformer.Tools.calculateBounds(feature); 269 | if(!feature.id) { 270 | throw new Error("Terraform.GeoStore : Feature does not have an id property"); 271 | } 272 | // TODO: if we want to be atomic and consistent, this should be truly synchronous 273 | self.index.insert({ 274 | x: bbox[0], 275 | y: bbox[1], 276 | w: Math.abs(bbox[0] - bbox[2]), 277 | h: Math.abs(bbox[1] - bbox[3]) 278 | }, feature.id); 279 | }; 280 | var syncIndexes = function(feature) { 281 | for (var j = 0; j < self._additional_indexes.length; j++) { 282 | sync.next(checkIndexAndAdd, self._additional_indexes[j], feature); 283 | } 284 | } 285 | 286 | if (!geojson.type.match(/Feature/)) { 287 | throw new Error("Terraform.GeoStore : only Features and FeatureCollections are supported"); 288 | } 289 | 290 | if (this._additional_indexes && this._additional_indexes.length) { 291 | sync = new Sync(); 292 | } 293 | 294 | // set a bounding box 295 | if(geojson.type === "FeatureCollection"){ 296 | for (i = 0; i < geojson.features.length; i++) { 297 | addFeature(geojson.features[i]); 298 | } 299 | } else { 300 | addFeature(geojson); 301 | } 302 | if (sync) { 303 | if(geojson.type === "FeatureCollection"){ 304 | for (i = 0; i < geojson.features.length; i++) { 305 | syncIndexes(geojson.features[i]); 306 | } 307 | finished = function (geo) { 308 | self.store.add(geo); 309 | }; 310 | } else { 311 | syncIndexes(geojson); 312 | finished = function (geo, cb) { 313 | self.store.add(geo, cb); 314 | }; 315 | } 316 | sync.next(finished, geojson); 317 | sync.start(callback); 318 | } else { 319 | this.store.add(geojson, callback); 320 | } 321 | }; 322 | 323 | GeoStore.prototype.remove = function(id, callback){ 324 | var self = this; 325 | this.get(id, bind(this, function(error, geojson){ 326 | if ( error ){ 327 | callback("Could not get feature to remove", null); 328 | } else { 329 | this.index.remove(geojson, id, bind(this, function(error, leaf){ 330 | if(error){ 331 | callback("Could not remove from index", null); 332 | } else { 333 | // check for additional indexes and add to them if there are property matches 334 | if (this._additional_indexes && this._additional_indexes.length) { 335 | sync = new Sync(); 336 | 337 | for (j = 0; j < this._additional_indexes.length; j++) { 338 | sync.next(checkIndexAndRemove, this._additional_indexes[j], geojson); 339 | } 340 | 341 | var finished = function (geo, cb) { 342 | self.store.remove(geo, cb); 343 | }; 344 | 345 | sync.next(finished, geojson); 346 | sync.start(callback); 347 | } else { 348 | this.store.remove(id, callback); 349 | } 350 | } 351 | })); 352 | } 353 | })); 354 | }; 355 | 356 | GeoStore.prototype._genericSpatialOperation = function (geojson, indexQuery, operationName, callback) { 357 | var indexSearchMethod, shapeGeometryTest; 358 | switch (operationName) { 359 | case 'within': 360 | indexSearchMethod = this.index.within; 361 | shapeGeometryTest = function(geometry, shape) { 362 | return geometry.within(shape); 363 | }; 364 | break; 365 | case 'contains': 366 | indexSearchMethod = this.index.search; 367 | shapeGeometryTest = function(geometry, shape) { 368 | return shape.within(geometry); 369 | }; 370 | break; 371 | case 'intersects': 372 | indexSearchMethod = this.index.search; 373 | shapeGeometryTest = function(geometry, shape) { 374 | return shape.intersects(geometry); 375 | }; 376 | break; 377 | } 378 | 379 | // make a new deferred 380 | var shape = new Terraformer.Primitive(geojson); 381 | 382 | // create our envelope 383 | var envelope = Terraformer.Tools.calculateEnvelope(shape); 384 | 385 | // search the index 386 | indexSearchMethod(envelope, bind(this, function(err, found){ 387 | var results = []; 388 | var completed = 0; 389 | var errors = 0; 390 | var self = this; 391 | var sync = new Sync(); 392 | var set; 393 | var i; 394 | 395 | // should we do set elimination with additional indexes? 396 | if (indexQuery && self._additional_indexes.length) { 397 | // convert "found" to an object with keys 398 | set = { }; 399 | 400 | for (i = 0; i < found.length; i++) { 401 | set[found[i]] = true; 402 | } 403 | 404 | // iterate through the queries, find the correct indexes, and apply them 405 | var keys = Object.keys(indexQuery); 406 | 407 | for (var j = 0; j < keys.length; j++) { 408 | for (i = 0; i < self._additional_indexes.length; i++) { 409 | // index property matches query 410 | if (self._additional_indexes[i].property === keys[j]) { 411 | var which = indexQuery[keys[j]], index = self._additional_indexes[i].index; 412 | 413 | sync.next(function (index, which, set, cb) { 414 | var next = this; 415 | eliminateForIndex(index, which, set, function (err, newSet) { 416 | set = newSet; 417 | cb(err); 418 | }); 419 | }, index, which, set); 420 | } 421 | } 422 | } 423 | } 424 | 425 | sync.next(function () { 426 | // if we have a set, it is our new "found" 427 | if (set) { 428 | found = Object.keys(set); 429 | } 430 | 431 | // the function to evalute results from the index 432 | var evaluate = function(primitive){ 433 | completed++; 434 | if ( primitive ){ 435 | var geometry = new Terraformer.Primitive(primitive.geometry); 436 | if (shapeGeometryTest(geometry, shape)){ 437 | if (self._stream) { 438 | if (completed === found.length) { 439 | if (operationName == "within") { 440 | self._stream.emit("done", primitive); 441 | } else { 442 | self._stream.emit("data", primitive); 443 | } 444 | self._stream.emit("end"); 445 | } else { 446 | self._stream.emit("data", primitive); 447 | } 448 | } else { 449 | results.push(primitive); 450 | } 451 | } 452 | 453 | if(completed >= found.length){ 454 | if(!errors){ 455 | if (self._stream) { 456 | self._stream = null; 457 | } else if (callback) { 458 | callback( null, results ); 459 | } 460 | } else { 461 | if (callback) { 462 | callback("Could not get all geometries", null); 463 | } 464 | } 465 | } 466 | } 467 | }; 468 | 469 | var error = function(){ 470 | completed++; 471 | errors++; 472 | if(completed >= found.length){ 473 | if (callback) { 474 | callback("Could not get all geometries", null); 475 | } 476 | } 477 | }; 478 | 479 | // for each result see if the polygon contains the point 480 | if(found && found.length){ 481 | var getCB = function(err, result){ 482 | if (err) { 483 | error(); 484 | } else { 485 | evaluate( result ); 486 | } 487 | }; 488 | 489 | for (var i = 0; i < found.length; i++) { 490 | self.get(found[i], getCB); 491 | } 492 | } else { 493 | if (callback) { 494 | callback(null, results); 495 | } 496 | } 497 | }); 498 | 499 | sync.start(); 500 | 501 | })); 502 | }; 503 | 504 | GeoStore.prototype.intersects = function (geojson) { 505 | var indexQuery; 506 | var args = Array.prototype.slice.call(arguments); 507 | args.shift(); 508 | 509 | var callback = args.pop(); 510 | if (args.length) { 511 | indexQuery = args[0]; 512 | } 513 | 514 | return this._genericSpatialOperation(geojson, indexQuery, 'intersects', callback); 515 | }; 516 | 517 | GeoStore.prototype.contains = function (geojson) { 518 | var indexQuery; 519 | var args = Array.prototype.slice.call(arguments); 520 | args.shift(); 521 | 522 | var callback = args.pop(); 523 | if (args.length) { 524 | indexQuery = args[0]; 525 | } 526 | 527 | return this._genericSpatialOperation(geojson, indexQuery, 'contains', callback); 528 | }; 529 | 530 | GeoStore.prototype.within = function(geojson){ 531 | var indexQuery; 532 | var args = Array.prototype.slice.call(arguments); 533 | args.shift(); 534 | var callback = args.pop(); 535 | if (args.length) { 536 | indexQuery = args[0]; 537 | } 538 | 539 | return this._genericSpatialOperation(geojson, indexQuery, 'within', callback); 540 | }; 541 | 542 | GeoStore.prototype.update = function(geojson, callback){ 543 | var feature = Terraformer.Primitive(geojson); 544 | 545 | if (feature.type !== "Feature") { 546 | throw new Error("Terraform.GeoStore : only Features and FeatureCollections are supported"); 547 | } 548 | 549 | if(!feature.id) { 550 | throw new Error("Terraform.GeoStore : Feature does not have an id property"); 551 | } 552 | 553 | this.get(feature.id, bind(this, function( error, oldFeatureGeoJSON ){ 554 | if ( error ){ 555 | callback("Could find feature", null); 556 | } else { 557 | var oldFeature = new Terraformer.Primitive(oldFeatureGeoJSON); 558 | this.index.remove(oldFeature.envelope(), oldFeature.id); 559 | this.index.insert(feature.envelope(), feature.id); 560 | this.store.update(feature, callback); 561 | } 562 | })); 563 | 564 | }; 565 | 566 | // gets an item by id 567 | GeoStore.prototype.get = function(id, callback){ 568 | this.store.get( id, callback ); 569 | }; 570 | 571 | GeoStore.prototype.createReadStream = function () { 572 | this._stream = new Stream(); 573 | return this._stream; 574 | }; 575 | 576 | // add an index 577 | GeoStore.prototype.addIndex = function(index) { 578 | this._additional_indexes.push(index); 579 | }; 580 | 581 | 582 | /* 583 | "crime": 584 | { 585 | "equals": "arson" 586 | } 587 | 588 | index -> specific index that references the property keyword 589 | query -> object containing the specific queries for the index 590 | set -> object containing keys of all of the id's matching currently 591 | 592 | callback -> object containing keys of all of the id's still matching: 593 | { 594 | 1: true, 595 | 23: true 596 | } 597 | 598 | TODO: add functionality for 599 | "crime": 600 | { 601 | "or": { 602 | "equals": "arson", 603 | "equals": "theft" 604 | } 605 | } 606 | */ 607 | function eliminateForIndex(index, query, set, callback) { 608 | var queryKeys = Object.keys(query); 609 | var count = 0; 610 | 611 | for (var i = 0; i < queryKeys.length; i++) { 612 | if (typeof index[queryKeys[i]] !== "function") { 613 | callback("Index does not have a method matching " + queryKeys[i]); 614 | return; 615 | } 616 | 617 | index[queryKeys[i]](query[i], function (err, data) { 618 | count++; 619 | 620 | if (err) { 621 | callback(err); 622 | 623 | // short-circuit the scan, we hit an error. this is fatal. 624 | count = queryKeys.length; 625 | return; 626 | } else { 627 | var setKeys = Object.keys(set); 628 | for (var j = 0; j < setKeys.length; j++) { 629 | if (!data[setKeys[j]]) { 630 | delete set[setKeys[j]]; 631 | } 632 | } 633 | } 634 | 635 | if (count === queryKeys.length) { 636 | callback(null, set); 637 | } 638 | }); 639 | } 640 | } 641 | 642 | exports.GeoStore = GeoStore; 643 | 644 | return exports; 645 | 646 | 647 | })); 648 | -------------------------------------------------------------------------------- /spec/geostoreSpec.js: -------------------------------------------------------------------------------- 1 | var gs; 2 | 3 | if(typeof module === "object"){ 4 | var Terraformer = require("terraformer"); 5 | Terraformer.RTree = require("terraformer-rtree").RTree; 6 | Terraformer.Store = {}; 7 | Terraformer.Store.Memory = require("../src/memory.js").Memory; 8 | Terraformer.GeoStore = require("../node/terraformer-geostore.js").GeoStore; 9 | var btree = require("terraformer-geostore-index-btree").BinarySearchTree; 10 | } 11 | 12 | describe("geostore", function() { 13 | describe("with a memory store and rtree", function(){ 14 | 15 | it("should throw an error when initalized without a store or index", function(){ 16 | expect(function() { 17 | gs = new Terraformer.GeoStore({}); 18 | }).toThrow(); 19 | expect(gs).toBeFalsy(); 20 | }); 21 | 22 | it("should create with a Memory store and an RTree", function(){ 23 | expect(function() { 24 | gs = new Terraformer.GeoStore({ 25 | store: new Terraformer.Store.Memory(), 26 | index: new Terraformer.RTree() 27 | }); 28 | }).not.toThrow(); 29 | expect(gs).toBeTruthy(); 30 | }); 31 | 32 | it("should throw an error when a feautre without an id is added", function(){ 33 | expect(function() { 34 | gs.add({"type":"Feature","properties":{"name":"Multnomah"},"geometry":{"type":"Polygon","coordinates":[[[-122.926547,45.725029],[-122.762239,45.730506],[-122.247407,45.549767],[-121.924267,45.648352],[-121.820205,45.462136],[-122.356945,45.462136],[-122.745808,45.434751],[-122.926547,45.725029]]]}}); 35 | }).toThrow(); 36 | }); 37 | 38 | it("should throw an error adding invalid features to a store", function(){ 39 | expect(function() { 40 | gs.add({ 41 | "type": "FeatureCollection", 42 | "features":[ 43 | {"type":"Polygon","coordinates":[[[-123.134671,45.779798],[-122.926547,45.725029],[-122.745808,45.434751],[-122.866301,45.319735],[-123.063471,45.401889],[-123.463287,45.434751],[-123.359225,45.779798],[-123.134671,45.779798]]]}, 44 | {"type":"Feature","properties":{"name":"Clackamas"},"geometry":{"type":"Polygon","coordinates":[[[-122.356945,45.462136],[-121.820205,45.462136],[-121.694236,45.259489],[-121.732574,44.887057],[-122.395284,44.887057],[-122.84987,45.259489],[-122.866301,45.319735],[-122.745808,45.434751],[-122.356945,45.462136]]]}} 45 | ] 46 | }); 47 | }).toThrow(); 48 | }); 49 | 50 | it("should throw an error when a GeoJSON object that is not a feature or a feautre collections is added", function(){ 51 | expect(function() { 52 | gs.add({"type":"Polygon","coordinates":[[[-122.926547,45.725029],[-122.762239,45.730506],[-122.247407,45.549767],[-121.924267,45.648352],[-121.820205,45.462136],[-122.356945,45.462136],[-122.745808,45.434751],[-122.926547,45.725029]]]}); 53 | }).toThrow(); 54 | }); 55 | 56 | it("should add features to a store", function(){ 57 | gs.add({"type":"Feature","id":"41051","properties":{"name":"Multnomah"},"geometry":{"type":"Polygon","coordinates":[[[-122.926547,45.725029],[-122.762239,45.730506],[-122.247407,45.549767],[-121.924267,45.648352],[-121.820205,45.462136],[-122.356945,45.462136],[-122.745808,45.434751],[-122.926547,45.725029]]]}}); 58 | expect(gs.store.data[41051]).toBeTruthy(); 59 | }); 60 | 61 | it("should add features to a store and run a callback", function(){ 62 | var spy = jasmine.createSpy(); 63 | gs.add({ 64 | "type": "FeatureCollection", 65 | "features":[ 66 | {"type":"Feature","id":"41067","properties":{"name":"Washington"},"geometry":{"type":"Polygon","coordinates":[[[-123.134671,45.779798],[-122.926547,45.725029],[-122.745808,45.434751],[-122.866301,45.319735],[-123.063471,45.401889],[-123.463287,45.434751],[-123.359225,45.779798],[-123.134671,45.779798]]]}}, 67 | {"type":"Feature","id":"41005","properties":{"name":"Clackamas"},"geometry":{"type":"Polygon","coordinates":[[[-122.356945,45.462136],[-121.820205,45.462136],[-121.694236,45.259489],[-121.732574,44.887057],[-122.395284,44.887057],[-122.84987,45.259489],[-122.866301,45.319735],[-122.745808,45.434751],[-122.356945,45.462136]]]}} 68 | ] 69 | }, spy); 70 | expect(spy.calls.count()).toEqual(1); 71 | expect(gs.store.data[41067]).toBeTruthy(); 72 | expect(gs.store.data[41005]).toBeTruthy(); 73 | }); 74 | 75 | it("should find no results", function(){ 76 | var result; 77 | gs.contains({ 78 | type:"Point", 79 | coordinates: [0, 0] 80 | }, function(error, found){ 81 | expect(found.length).toEqual(0); 82 | }); 83 | }); 84 | 85 | it("should find one result", function(){ 86 | var result; 87 | gs.contains({ 88 | type:"Point", 89 | coordinates: [-122.676048, 45.516544] 90 | }, function(error, found){ 91 | expect(found.length).toEqual(1); 92 | expect(found[0].id).toEqual("41051"); 93 | }); 94 | }); 95 | 96 | 97 | it("should remove a feature", function(){ 98 | var result; 99 | var spy = jasmine.createSpy(); 100 | gs.remove("41051", spy); 101 | expect(spy.calls.count()).toEqual(1); 102 | expect(gs.store.data[41051]).toBeFalsy(); 103 | expect(gs.store.data[41067]).toBeTruthy(); 104 | }); 105 | 106 | it("shouldn't find any results", function(){ 107 | var result; 108 | gs.contains({ 109 | type:"Point", 110 | coordinates: [-122.676048, 45.516544] 111 | }, function(error, found){ 112 | expect(found.length).toEqual(0); 113 | }); 114 | }); 115 | 116 | it("should get a single result by id", function(){ 117 | var result; 118 | gs.get("41067", function(error, found){ 119 | expect(found.id).toEqual("41067"); 120 | }); 121 | }); 122 | 123 | it("should update a feature and run a successful query", function(){ 124 | var result; 125 | var spy = jasmine.createSpy(); 126 | gs.update({"type":"Feature","id":"41067","properties":{"name":"Multnomah"},"geometry":{"type":"Polygon","coordinates":[[[-122.926547,45.725029],[-122.762239,45.730506],[-122.247407,45.549767],[-121.924267,45.648352],[-121.820205,45.462136],[-122.356945,45.462136],[-122.745808,45.434751],[-122.926547,45.725029]]]}}, spy); 127 | expect(spy.calls.count()).toEqual(1); 128 | gs.contains({ 129 | type:"Point", 130 | coordinates: [-122.676048, 45.516544] 131 | }, function(error, found){ 132 | expect(found.length).toEqual(1); 133 | expect(found[0].id).toEqual("41067"); 134 | }); 135 | }); 136 | 137 | it("should update features in store and run a callback", function(){ 138 | var spy = jasmine.createSpy(); 139 | gs.update({"type":"Feature","id":"41067","properties":{"name":"Multnomah"},"geometry":{"type":"Polygon","coordinates":[[[-122.926547,45.725029],[-122.762239,45.730506],[-122.247407,45.549767],[-121.924267,45.648352],[-121.820205,45.462136],[-122.356945,45.462136],[-122.745808,45.434751],[-122.926547,45.725029]]]}}, spy); 140 | expect(spy.calls.count()).toEqual(1); 141 | }); 142 | 143 | var serial; 144 | 145 | it("should serialize a Memory store", function(){ 146 | var callback = jasmine.createSpy("callback"); 147 | gs.store.serialize(callback); 148 | expect(callback).toHaveBeenCalled(); 149 | expect(callback).toHaveBeenCalledWith(null, JSON.stringify(gs.store.data)); 150 | }); 151 | 152 | it("should deserialize a memory store", function(){ 153 | var spy = jasmine.createSpy(); 154 | var serial; 155 | gs.store.serialize(function(error, data){ 156 | serial = data; 157 | }); 158 | gs = new Terraformer.GeoStore({ 159 | store: new Terraformer.Store.Memory().deserialize(serial), 160 | index: new Terraformer.RTree() 161 | }); 162 | expect(gs.store.data[41005]).toBeTruthy(); 163 | expect(gs.store.data[41067]).toBeTruthy(); 164 | }); 165 | 166 | var badStore = new Terraformer.GeoStore({ 167 | store: { 168 | get: function(id, callback){ 169 | return callback("ERROR", null); 170 | }, 171 | add: function(geo, callback){ 172 | return callback("ERROR", null); 173 | }, 174 | remove: function(id, callback){ 175 | return callback("ERROR", null); 176 | }, 177 | update: function(geo, callback){ 178 | return callback("ERROR", null); 179 | } 180 | }, 181 | index: new Terraformer.RTree() 182 | }); 183 | 184 | it("should run an error callback when the store rejects the deferred when adding an item", function(){ 185 | var spy = jasmine.createSpy(); 186 | badStore.add({"type":"Feature","id":"41067","properties":{"name":"Multnomah"},"geometry":{"type":"Polygon","coordinates":[[[-122.926547,45.725029],[-122.762239,45.730506],[-122.247407,45.549767],[-121.924267,45.648352],[-121.820205,45.462136],[-122.356945,45.462136],[-122.745808,45.434751],[-122.926547,45.725029]]]}}, spy); 187 | expect(spy).toHaveBeenCalledWith("ERROR", null); 188 | }); 189 | 190 | it("should run an error callback when the store rejects the deferred when updating an item", function(){ 191 | var spy = jasmine.createSpy(); 192 | badStore.update({"type":"Feature","id":"41067","properties":{"name":"Multnomah"},"geometry":{"type":"Polygon","coordinates":[[[-122.926547,45.725029],[-122.762239,45.730506],[-122.247407,45.549767],[-121.924267,45.648352],[-121.820205,45.462136],[-122.356945,45.462136],[-122.745808,45.434751],[-122.926547,45.725029]]]}}, spy); 193 | expect(spy).toHaveBeenCalledWith('Could find feature', null); 194 | }); 195 | 196 | it("should return an error in the callback when the store cant get an item", function(){ 197 | var spy = jasmine.createSpy(); 198 | badStore.get("41067", spy); 199 | expect(spy).toHaveBeenCalledWith("ERROR", null); 200 | }); 201 | 202 | it("should return an error in the callback when the store cant find a feature to remove", function(){ 203 | var spy = jasmine.createSpy(); 204 | badStore.remove("41067", spy); 205 | expect(spy).toHaveBeenCalledWith('Could not get feature to remove', null); 206 | }); 207 | 208 | it("should run an error callback when the store rejects the deferred when querying an item", function(){ 209 | var spy = jasmine.createSpy(); 210 | badStore.contains({ 211 | type:"Point", 212 | coordinates: [-122.676048, 45.516544] 213 | }, spy); 214 | expect(spy).toHaveBeenCalledWith("Could not get all geometries", null); 215 | }); 216 | }); 217 | 218 | if(typeof navigator !== "undefined"){ 219 | describe("with a Memory store and rtree", function(){ 220 | var gs; 221 | 222 | it("should create with a Memory and an RTree", function(){ 223 | expect(function() { 224 | gs = new Terraformer.GeoStore({ 225 | store: new Terraformer.Store.Memory(), 226 | index: new Terraformer.RTree() 227 | }); 228 | }).not.toThrow(); 229 | expect(gs).toBeTruthy(); 230 | }); 231 | 232 | it("should find one result", function(){ 233 | var result; 234 | gs.add({"type":"Feature","id":"41051","properties":{"name":"Multnomah"},"geometry":{"type":"Polygon","coordinates":[[[-122.926547,45.725029],[-122.762239,45.730506],[-122.247407,45.549767],[-121.924267,45.648352],[-121.820205,45.462136],[-122.356945,45.462136],[-122.745808,45.434751],[-122.926547,45.725029]]]}}); 235 | gs.add({"type":"Feature","id":"41067","properties":{"name":"Washington"},"geometry":{"type":"Polygon","coordinates":[[[-123.134671,45.779798],[-122.926547,45.725029],[-122.745808,45.434751],[-122.866301,45.319735],[-123.063471,45.401889],[-123.463287,45.434751],[-123.359225,45.779798],[-123.134671,45.779798]]]}}); 236 | gs.contains({ 237 | type:"Point", 238 | coordinates: [-122.676048, 45.516544] 239 | }, function(error, found){ 240 | expect(found.length).toEqual(1); 241 | expect(found[0].id).toEqual("41051"); 242 | }); 243 | }); 244 | 245 | it("shouldn't find any results if a feature is removed", function(){ 246 | var result; 247 | gs.remove("41051"); 248 | gs.within({ 249 | type:"Point", 250 | coordinates: [-122.676048, 45.516544] 251 | }, function(error, found){ 252 | expect(found.length).toEqual(0); 253 | }); 254 | }); 255 | 256 | it("should update a feature and run a successful query", function(){ 257 | var result; 258 | gs.update({"type":"Feature","id":"41067","properties":{"name":"Multnomah"},"geometry":{"type":"Polygon","coordinates":[[[-122.926547,45.725029],[-122.762239,45.730506],[-122.247407,45.549767],[-121.924267,45.648352],[-121.820205,45.462136],[-122.356945,45.462136],[-122.745808,45.434751],[-122.926547,45.725029]]]}}); 259 | gs.contains({ 260 | type:"Point", 261 | coordinates: [-122.676048, 45.516544] 262 | }, function( error, found){ 263 | expect(found.length).toEqual(1); 264 | expect(found[0].id).toEqual("41067"); 265 | }); 266 | }); 267 | 268 | it("should throw an error when a feautre without an id is updated", function(){ 269 | expect(function() { 270 | gs.update({"type":"Feature","properties":{"name":"Multnomah"},"geometry":{"type":"Polygon","coordinates":[[[-122.926547,45.725029],[-122.762239,45.730506],[-122.247407,45.549767],[-121.924267,45.648352],[-121.820205,45.462136],[-122.356945,45.462136],[-122.745808,45.434751],[-122.926547,45.725029]]]}}); 271 | }).toThrow(); 272 | }); 273 | 274 | it("should throw an error when a GeoJSON object that is not a feature or a feature collection is updated", function(){ 275 | expect(function() { 276 | gs.update({"type":"Polygon","coordinates":[[[-122.926547,45.725029],[-122.762239,45.730506],[-122.247407,45.549767],[-121.924267,45.648352],[-121.820205,45.462136],[-122.356945,45.462136],[-122.745808,45.434751],[-122.926547,45.725029]]]}); 277 | }).toThrow(); 278 | }); 279 | 280 | it("should serialize a Memory store", function(){ 281 | var callback = jasmine.createSpy("callback"); 282 | gs.store.serialize(callback); 283 | expect(callback).toHaveBeenCalled(); 284 | }); 285 | }); 286 | 287 | } 288 | 289 | describe("with a memory store and rtree and within", function(){ 290 | var gs; 291 | 292 | it("should create with a Memory store and an RTree", function(){ 293 | expect(function() { 294 | gs = new Terraformer.GeoStore({ 295 | store: new Terraformer.Store.Memory(), 296 | index: new Terraformer.RTree() 297 | }); 298 | }).not.toThrow(); 299 | expect(gs).toBeTruthy(); 300 | }); 301 | 302 | it("should be able to add coordinates", function(){ 303 | expect(function() { 304 | gs.add({ "id": 1, "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [ [ [ -122.655849602879201, 45.538304922840894, 0.0 ], [ -122.655691867426299, 45.538304448196108, 0.0 ], [ -122.655692285611451, 45.538236031205983, 0.0 ], [ -122.655850019628431, 45.538236506773742, 0.0 ], [ -122.655849602879201, 45.538304922840894, 0.0 ] ] ] } }); 305 | gs.add({ "id": 2, "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [ [ [ -122.589480827112766, 45.477987695417717, 0.0 ], [ -122.589481271327131, 45.477926424718063, 0.0 ], [ -122.589631424206658, 45.477926964033657, 0.0 ], [ -122.589630980154141, 45.477988234733886, 0.0 ], [ -122.589480827112766, 45.477987695417717, 0.0 ] ] ] } }); 306 | }).not.toThrow(); 307 | expect(gs).toBeTruthy(); 308 | }); 309 | 310 | it("should be able to find the correct id using within", function(){ 311 | gs.within({ 312 | "type": "Polygon", 313 | "coordinates": [ [ [ -122.655849602879201, 45.538304922840894, 0.0 ], [ -122.655691867426299, 45.538304448196108, 0.0 ], [ -122.655692285611451, 45.538236031205983, 0.0 ], [ -122.655850019628431, 45.538236506773742, 0.0 ], [ -122.655849602879201, 45.538304922840894, 0.0 ] ] ] 314 | }, function (err, res) { 315 | expect(res[0].id).toEqual(1); 316 | }); 317 | }); 318 | }); 319 | 320 | describe("with a memory store and streams", function(){ 321 | var gs, stream; 322 | 323 | it("should create with a Memory store and an RTree", function(){ 324 | expect(function() { 325 | gs = new Terraformer.GeoStore({ 326 | store: new Terraformer.Store.Memory(), 327 | index: new Terraformer.RTree() 328 | }); 329 | }).not.toThrow(); 330 | expect(gs).toBeTruthy(); 331 | }); 332 | 333 | it("should be able to add coordinates", function(){ 334 | expect(function() { 335 | gs.add({ "id": 1, "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [ [ [ -122.655849602879201, 45.538304922840894, 0.0 ], [ -122.655691867426299, 45.538304448196108, 0.0 ], [ -122.655692285611451, 45.538236031205983, 0.0 ], [ -122.655850019628431, 45.538236506773742, 0.0 ], [ -122.655849602879201, 45.538304922840894, 0.0 ] ] ] } }); 336 | gs.add({ "id": 2, "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [ [ [ -122.589480827112766, 45.477987695417717, 0.0 ], [ -122.589481271327131, 45.477926424718063, 0.0 ], [ -122.589631424206658, 45.477926964033657, 0.0 ], [ -122.589630980154141, 45.477988234733886, 0.0 ], [ -122.589480827112766, 45.477987695417717, 0.0 ] ] ] } }); 337 | }).not.toThrow(); 338 | expect(gs).toBeTruthy(); 339 | }); 340 | 341 | it("should be able to create a Stream", function(){ 342 | expect(stream = gs.createReadStream()).toBeTruthy(); 343 | }); 344 | }); 345 | 346 | describe("with a memory store and alternate indexes and within", function(){ 347 | var gs; 348 | 349 | it("should create with a Memory store and an RTree", function(){ 350 | expect(function() { 351 | gs = new Terraformer.GeoStore({ 352 | store: new Terraformer.Store.Memory(), 353 | index: new Terraformer.RTree() 354 | }); 355 | }).not.toThrow(); 356 | expect(gs).toBeTruthy(); 357 | }); 358 | 359 | it("should be able to add coordinates", function(){ 360 | expect(function() { 361 | gs.add({ "id": 1, "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [ [ [ -122.655849602879201, 45.538304922840894, 0.0 ], [ -122.655691867426299, 45.538304448196108, 0.0 ], [ -122.655692285611451, 45.538236031205983, 0.0 ], [ -122.655850019628431, 45.538236506773742, 0.0 ], [ -122.655849602879201, 45.538304922840894, 0.0 ] ] ] } }); 362 | gs.add({ "id": 2, "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [ [ [ -122.589480827112766, 45.477987695417717, 0.0 ], [ -122.589481271327131, 45.477926424718063, 0.0 ], [ -122.589631424206658, 45.477926964033657, 0.0 ], [ -122.589630980154141, 45.477988234733886, 0.0 ], [ -122.589480827112766, 45.477987695417717, 0.0 ] ] ] } }); 363 | }).not.toThrow(); 364 | expect(gs).toBeTruthy(); 365 | }); 366 | 367 | it("should be able to find the correct id using within", function(){ 368 | gs.within({ 369 | "type": "Polygon", 370 | "coordinates": [ [ [ -122.655849602879201, 45.538304922840894, 0.0 ], [ -122.655691867426299, 45.538304448196108, 0.0 ], [ -122.655692285611451, 45.538236031205983, 0.0 ], [ -122.655850019628431, 45.538236506773742, 0.0 ], [ -122.655849602879201, 45.538304922840894, 0.0 ] ] ] 371 | }, function (err, res) { 372 | expect(res[0].id).toEqual(1); 373 | }); 374 | }); 375 | 376 | it("should be able to eliminate the result with an alternate index", function(){ 377 | gs.addIndex({ property: "crime", index: function (value, callback) { callback(null, { 1: false }); }}); 378 | gs.within({ 379 | "type": "Polygon", 380 | "coordinates": [ [ [ -122.655849602879201, 45.538304922840894, 0.0 ], [ -122.655691867426299, 45.538304448196108, 0.0 ], [ -122.655692285611451, 45.538236031205983, 0.0 ], [ -122.655850019628431, 45.538236506773742, 0.0 ], [ -122.655849602879201, 45.538304922840894, 0.0 ] ] ] 381 | }, 382 | { 383 | "crime": 384 | { 385 | "equals": "arson" 386 | } 387 | }, 388 | function (err, res) { 389 | expect(err).toEqual(undefined); 390 | expect(res.length).toEqual(0); 391 | }); 392 | }); 393 | 394 | it("should be able to not eliminate the result with an alternate index", function(){ 395 | gs._alternate_indexes = [ ]; 396 | gs.addIndex({ property: "crime", index: function (value, callback) { callback(null, { 1: true }); }}); 397 | gs.within({ 398 | "type": "Polygon", 399 | "coordinates": [ [ [ -122.655849602879201, 45.538304922840894, 0.0 ], [ -122.655691867426299, 45.538304448196108, 0.0 ], [ -122.655692285611451, 45.538236031205983, 0.0 ], [ -122.655850019628431, 45.538236506773742, 0.0 ], [ -122.655849602879201, 45.538304922840894, 0.0 ] ] ] 400 | }, 401 | { 402 | "crime": 403 | { 404 | "equals": "arson" 405 | } 406 | }, 407 | function (err, res) { 408 | if (!err) { 409 | expect(res.length).toEqual(1); 410 | } 411 | }); 412 | }); 413 | 414 | }); 415 | 416 | describe("with a memory store and alternate indexes and contains", function(){ 417 | var gs; 418 | 419 | it("should create with a Memory store and an RTree", function(){ 420 | expect(function() { 421 | gs = new Terraformer.GeoStore({ 422 | store: new Terraformer.Store.Memory(), 423 | index: new Terraformer.RTree() 424 | }); 425 | }).not.toThrow(); 426 | expect(gs).toBeTruthy(); 427 | }); 428 | 429 | it("should be able to add coordinates", function(){ 430 | expect(function() { 431 | gs.add({"type":"Feature","id":1,"properties":{"name":"Multnomah"},"geometry":{"type":"Polygon","coordinates":[[[-122.926547,45.725029],[-122.762239,45.730506],[-122.247407,45.549767],[-121.924267,45.648352],[-121.820205,45.462136],[-122.356945,45.462136],[-122.745808,45.434751],[-122.926547,45.725029]]]}}); 432 | gs.add({"type":"Feature","id":2,"properties":{"name":"Washington"},"geometry":{"type":"Polygon","coordinates":[[[-123.134671,45.779798],[-122.926547,45.725029],[-122.745808,45.434751],[-122.866301,45.319735],[-123.063471,45.401889],[-123.463287,45.434751],[-123.359225,45.779798],[-123.134671,45.779798]]]}}); 433 | }).not.toThrow(); 434 | expect(gs).toBeTruthy(); 435 | }); 436 | 437 | it("should find no results", function(){ 438 | var result; 439 | gs.contains({ 440 | type:"Point", 441 | coordinates: [0, 0] 442 | }, function(error, found){ 443 | expect(found.length).toEqual(0); 444 | }); 445 | }); 446 | 447 | it("should find one result", function(){ 448 | var result; 449 | gs.contains({ 450 | type:"Point", 451 | coordinates: [-122.676048, 45.516544] 452 | }, function(error, found){ 453 | expect(found.length).toEqual(1); 454 | expect(found[0].id).toEqual(1); 455 | }); 456 | }); 457 | 458 | 459 | it("should be able to eliminate the result with an alternate index", function(){ 460 | gs.addIndex({ property: "crime", index: function (value, callback) { callback(null, { 1: false }); }}); 461 | var result; 462 | gs.contains({ 463 | type:"Point", 464 | coordinates: [-122.676048, 45.516544] 465 | }, 466 | { 467 | "crime": 468 | { 469 | "equals": "arson" 470 | } 471 | }, 472 | function(error, found){ 473 | expect(found.length).toEqual(0); 474 | }); 475 | }); 476 | 477 | it("should be able to not eliminate the result with an alternate index", function(){ 478 | gs._alternate_indexes = [ ]; 479 | gs.addIndex({ property: "crime", index: function (value, callback) { callback(null, { 1: true }); }}); 480 | gs.contains({ 481 | "type": "Polygon", 482 | "coordinates": [ [ [ -122.655849602879201, 45.538304922840894, 0.0 ], [ -122.655691867426299, 45.538304448196108, 0.0 ], [ -122.655692285611451, 45.538236031205983, 0.0 ], [ -122.655850019628431, 45.538236506773742, 0.0 ], [ -122.655849602879201, 45.538304922840894, 0.0 ] ] ] 483 | }, 484 | { 485 | "crime": 486 | { 487 | "equals": "arson" 488 | } 489 | }, 490 | function (err, res) { 491 | expect(res.length).toEqual(1); 492 | }); 493 | }); 494 | }); 495 | 496 | if (btree) { 497 | describe("btree tests", function () { 498 | it("should be able return the correct Feature with an alternate index after added", function(){ 499 | gs = new Terraformer.GeoStore({ 500 | store: new Terraformer.Store.Memory(), 501 | index: new Terraformer.RTree() 502 | }); 503 | 504 | 505 | gs.addIndex({ property: "name", index: new btree() }); 506 | 507 | gs.add({"type":"Feature","id":1,"properties":{"name":"Multnomah"},"geometry":{"type":"Polygon","coordinates":[[[-122.926547,45.725029],[-122.762239,45.730506],[-122.247407,45.549767],[-121.924267,45.648352],[-121.820205,45.462136],[-122.356945,45.462136],[-122.745808,45.434751],[-122.926547,45.725029]]]}}); 508 | gs.add({"type":"Feature","id":2,"properties":{"name":"Washington"},"geometry":{"type":"Polygon","coordinates":[[[-123.134671,45.779798],[-122.926547,45.725029],[-122.745808,45.434751],[-122.866301,45.319735],[-123.063471,45.401889],[-123.463287,45.434751],[-123.359225,45.779798],[-123.134671,45.779798]]]}}); 509 | 510 | gs.within({ 511 | "type": "Polygon", 512 | "coordinates": [ [ [ -121, 45, 0.0 ], [ -123, 45, 0.0 ], [ -123, 46, 0.0 ], [ -121, 46, 0.0 ], [ -121, 45, 0.0 ] ] ] 513 | }, 514 | { 515 | "name": 516 | { 517 | "equals": "Multnomah" 518 | } 519 | }, 520 | function (err, res) { 521 | expect(err).toEqual(null); 522 | expect(res.length).toEqual(1); 523 | expect(res[0].id).toEqual(1); 524 | }); 525 | }); 526 | 527 | it("should be able remove an entry and have it remove from the alternate index", function(){ 528 | gs = new Terraformer.GeoStore({ 529 | store: new Terraformer.Store.Memory(), 530 | index: new Terraformer.RTree() 531 | }); 532 | 533 | var idx = new btree(); 534 | 535 | gs.addIndex({ property: "name", index: idx }); 536 | 537 | gs.add({"type":"Feature","id":1,"properties":{"name":"Multnomah"},"geometry":{"type":"Polygon","coordinates":[[[-122.926547,45.725029],[-122.762239,45.730506],[-122.247407,45.549767],[-121.924267,45.648352],[-121.820205,45.462136],[-122.356945,45.462136],[-122.745808,45.434751],[-122.926547,45.725029]]]}}); 538 | gs.add({"type":"Feature","id":2,"properties":{"name":"Washington"},"geometry":{"type":"Polygon","coordinates":[[[-123.134671,45.779798],[-122.926547,45.725029],[-122.745808,45.434751],[-122.866301,45.319735],[-123.063471,45.401889],[-123.463287,45.434751],[-123.359225,45.779798],[-123.134671,45.779798]]]}}); 539 | 540 | gs.remove(2); 541 | idx.contains(2, function (err, data) { 542 | expect(data).toEqual(false); 543 | }); 544 | }); 545 | }); 546 | } 547 | }); 548 | --------------------------------------------------------------------------------