├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── lib └── utils.js ├── package.json └── test ├── fixtures ├── count.json ├── countFailedPaging.json ├── countNoPaging.json ├── countPaging.json ├── countStatsFail.json ├── features-empty.json ├── features.json ├── hostedLayerInfo.json ├── ids10.0.json ├── idsStatsFail.json ├── layer.json ├── layer10.0.json ├── layerInfo.json ├── layerNoPaging.json ├── layerPaging.json ├── layerStatsFail.json ├── objectIds.json ├── page.json ├── secured.json ├── serviceInfo.json ├── stats.json ├── statsCaps.json ├── statsFail.json ├── statsFailedPaging.json ├── statsNoPaging.json └── uncompressed.json └── index.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['standard'] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | tags 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'stable' 4 | sudo: false 5 | cache: 6 | directories: 7 | - node_modules 8 | script: 9 | - npm test 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## [1.6.2] - 2019-11-05 6 | ### Changed 7 | * Throw `TypeError` when URL is not a FeatureServer or MapServer with optional layer 8 | 9 | ## [1.6.1] - 2018-11-28 10 | ### Changed 11 | * Double check pagination support with a request for the second page 12 | 13 | ## [1.6.0] - 2018-06-19 14 | ### Added 15 | * Check for an option `outSR` and if provided use it to transform data; if no outSR option default to `outSR=4326` 16 | 17 | ## [1.5.13] - 2018-01-31 18 | ### Fixed 19 | * Parse out html at the end of an otherwise valid response 20 | 21 | ## [1.5.12] - 2017-12-29 22 | ### Changed 23 | * Z value is requested when paging 24 | 25 | ## [1.5.11] - 2016-10-23 26 | ### Fixed 27 | * Use options.size to restrict max 28 | 29 | ## [1.5.10] - 2016-10-23 30 | ### Changed 31 | * Don't use forever agent on http requests 32 | 33 | ## [1.5.9] - 2016-10-20 34 | ### Changed 35 | * Use request library instead of hand-rolled http(s) requests 36 | 37 | ## [1.5.8] - 2016-10-16 38 | ### Changed 39 | * Don't set min max OIDs for a single page request 40 | 41 | ## [1.5.7] - 2016-04-07 42 | ### Changed 43 | * Remove unneeded try/catch 44 | 45 | ## [1.5.6] - 2016-04-06 46 | ### Fixed 47 | * Get correct objectID range even when field names are unexpectedly capitalized 48 | 49 | ## [1.5.5] - 2016-03-31 50 | ### Changed 51 | * Exposed `getObjectIdRange` as public function 52 | 53 | ### Fixed 54 | * Corrected bug in statistics paging strategy 55 | 56 | ## [1.5.4] - 2016-03-30 57 | ### Fixed 58 | * Don't throw exception when passed in layer is of type numeric 59 | 60 | ## [1.5.3] - 2016-03-23 61 | ### Changed 62 | * A passed-in layer will always override what's in the url 63 | 64 | ## [1.5.2] - 2016-03-03 65 | ### Fixed 66 | * Get the object id field if it's explicitly in info properties 67 | * resultOffset strategy was reversed 68 | 69 | ## [1.5.1] - 2016-03-02 70 | ### Changed 71 | * Don't use resultOffset for services not hosted on ArcGIS Online, it's not reliable 72 | * Decode streaming server response 73 | 74 | ## [1.5.0] - 2016-02-28 75 | ### Added 76 | * Service Info, Layer Info, and Metadata are memoized 77 | * Can call `info`, `layerInfo`, and `metadata` without a callback when info is memoized 78 | 79 | ### Changed 80 | * Concurrency is set during paging process 81 | * URL parsing for services is more precise 82 | 83 | ## [1.4.6] - 2016-01-20 84 | ### Fixed 85 | * Guard against missing ObjectID field when trying to page 86 | * Remove unused dep 87 | 88 | ## [1.4.5] - 2016-01-13 89 | ### Changed 90 | * Throttle concurrency down on every 2 failed requests 91 | * Throttle concurrency up on every 10 successful requests 92 | 93 | ## [1.4.4] - 2016-01-07 94 | ### Fixed 95 | * Removed bad reference to `this` 96 | 97 | ## [1.4.3] - 2016-01-04 98 | ### Changed 99 | * Concurrency is throttled when encountering errors in the paging queue 100 | ### Fixed 101 | * Urls and Layers are parsed correctly on service init 102 | 103 | ## [1.4.2] - 2015-10-13 104 | ### Changed 105 | * Removed callback from parse try statement 106 | 107 | ## [1.4.1] - 2015-10-13 108 | ### Fixed 109 | * Removed log statement that was causing the process to crash 110 | 111 | ## [1.4.0] - 2015-10-12 112 | ### Added 113 | * Backoff time for failed requests can now be set as an option 114 | * `service.info` returns information about the feature or map service 115 | 116 | ### Changed 117 | * Return the page that was successfully completed when getting features 118 | * Response parsing has been moved to its own function and the callback is no longer wrapped in a try/catch 119 | 120 | ### Fixed 121 | * No longer failing to parse multi-chunk feature service responses that are not compressed 122 | 123 | ## [1.3.1] 124 | ### Fixed 125 | * Log retry urls correclty 126 | 127 | ## [1.3.0] 128 | ### Added 129 | * Allow a logger to be passed in through options.logger when initializing a new service 130 | 131 | ### Fixed 132 | * Don't throw exception when there is no error in the json returned from server 133 | * Catch non-json responses explicitly 134 | * Don't use zlib methods unsupported in node 0.10.* in testing 135 | * Error timestamps are all written in same case 136 | 137 | ## [1.2.7] - 2015-09-03 138 | ### Fixed 139 | * Correct logic for correcting against poorly instantiated FeatureServices 140 | 141 | ## [1.2.6] - 2015-09-02 142 | ### Fixed 143 | * Don't set a layer id with a query string 144 | * Don't set a url with a query string 145 | 146 | ## [1.2.5] - 2015-09-02 147 | ### Fixed 148 | * Uncaught error when `json.error` doesn't exist in `FeatureService.layerInfo` 149 | 150 | ## [1.2.4] - 2015-09-01 151 | ### Changed 152 | * Restrict max record count to 5000 or less 153 | 154 | ### Fixed 155 | * Pages generated from statistics use the correct layer when index > 0 156 | 157 | ## [1.2.3] - 2015-08-31 158 | ### Fixed 159 | * Do not exclude object ids === 0 when building pages 160 | 161 | ## [1.2.2] - 2015-08-26 162 | ### Changed 163 | * Errors after timeouts conform to standard 164 | * Errors on metadata requests conform to standard 165 | 166 | ### Fixed 167 | * Requests are ended correctly after timeouts 168 | 169 | ## [1.2.1] - 2015-08-17 170 | ### Changed 171 | * All requests accept gzip or deflate compressed 172 | * Requests are decoded async (for compatibility with node < 0.11.12) 173 | 174 | ### Fixed 175 | * Pages are built correctly for layers with an index > 0 176 | 177 | ## [1.2.0] - 2015-08-11 178 | ### Fixed 179 | * Pages based on object ids are now formed correctly 180 | 181 | ### Changed 182 | * All errors returned to the requestor are standardized 183 | 184 | ## [1.1.1] - 2015-08-10 185 | ### Fixed 186 | * Build dist for changes 187 | 188 | ## [1.1.0] - 2015-08-10 189 | ### Added 190 | * New fixtures and integration tests for paging 191 | * Support for paging layers from server version 10.0 192 | * New fixtures and tests for decoding 193 | 194 | ### Changed 195 | * Refactored paging strategy 196 | * Moved feature request decoding into isolated function 197 | 198 | ### Fixed 199 | * Catch errors that come on 200 responses 200 | * Errors are reported correctly up the chain 201 | * Retries for all errors 202 | 203 | ## [1.0.0] - 2015-08-07 204 | ### Added 205 | * New function gets objectID from service info 206 | 207 | ### Changed 208 | * Moved to koopjs github organization 209 | 210 | ### Fixed 211 | * detach http/https modules from FeatureService instances 212 | 213 | ## [0.2.0] - 2015-08-06 214 | ### Added 215 | * Feature requests time out 216 | 217 | ### Changed 218 | * Timeout set at 90 seconds 219 | 220 | ### Fixed 221 | * Reference to non-existent functions 222 | 223 | ## [0.1.0] - 2015-08-05 224 | ### Added 225 | * Feature service requests time out after 5 minutes of inactivity by default 226 | 227 | ### Changed 228 | * TCP sockets are kept alive 229 | 230 | ## [0.0.4] - 2015-07-29 231 | ### Fixed 232 | * request method was calling the callback twice when it wrapped a callback in a try/catch 233 | * passing min and max to statsUrl for creating stats in pages method 234 | 235 | ## [0.0.3] - 2015-07-28 236 | ### Fixed 237 | * A change made in v0.0.2 broke pagination. Its now fixed and a test for pages was added. 238 | 239 | ## [0.0.2] - 2015-07-27 240 | ### Added 241 | * A method for making statistics calls to a service 242 | * Support for using the module in the browser 243 | 244 | ### Changed 245 | * http requests are all routed through the core http/https libs now 246 | 247 | ## [0.0.1] - 2015-07-22 248 | ### Added 249 | * Code for requesting data from FeatureServices 250 | * Tests on most methods 251 | 252 | [1.6.2]: https://github.com/koopjs/featureservice/compare/v1.6.1...v1.6.2 253 | [1.6.1]: https://github.com/koopjs/featureservice/compare/v1.6.0...v1.6.1 254 | [1.6.0]: https://github.com/koopjs/featureservice/compare/v1.5.13...v1.6.0 255 | [1.5.13]: https://github.com/koopjs/featureservice/compare/v1.5.12...v1.5.13 256 | [1.5.12]: https://github.com/koopjs/featureservice/compare/v1.5.11...v1.5.12 257 | [1.5.11]: https://github.com/koopjs/featureservice/compare/v1.5.10...v1.5.11 258 | [1.5.10]: https://github.com/koopjs/featureservice/compare/v1.5.9...v1.5.10 259 | [1.5.9]: https://github.com/koopjs/featureservice/compare/v1.5.8...v1.5.9 260 | [1.5.8]: https://github.com/koopjs/featureservice/compare/v1.5.7...v1.5.8 261 | [1.5.7]: https://github.com/koopjs/featureservice/compare/v1.5.6...v1.5.7 262 | [1.5.6]: https://github.com/koopjs/featureservice/compare/v1.5.5...v1.5.6 263 | [1.5.5]: https://github.com/koopjs/featureservice/compare/v1.5.4...v1.5.5 264 | [1.5.4]: https://github.com/koopjs/featureservice/compare/v1.5.3...v1.5.4 265 | [1.5.3]: https://github.com/koopjs/featureservice/compare/v1.5.2...v1.5.3 266 | [1.5.2]: https://github.com/koopjs/featureservice/compare/v1.5.1...v1.5.2 267 | [1.5.1]: https://github.com/koopjs/featureservice/compare/v1.5.0...v1.5.1 268 | [1.5.0]: https://github.com/koopjs/featureservice/compare/v1.4.6...v1.5.0 269 | [1.4.6]: https://github.com/koopjs/featureservice/compare/v1.4.5...v1.4.6 270 | [1.4.5]: https://github.com/koopjs/featureservice/compare/v1.4.4...v1.4.5 271 | [1.4.4]: https://github.com/koopjs/featureservice/compare/v1.4.3...v1.4.4 272 | [1.4.3]: https://github.com/koopjs/featureservice/compare/v1.4.2...v1.4.3 273 | [1.4.2]: https://github.com/koopjs/featureservice/compare/v1.4.1...v1.4.2 274 | [1.4.1]: https://github.com/koopjs/featureservice/compare/v1.4.0...v1.4.1 275 | [1.4.0]: https://github.com/koopjs/featureservice/compare/v1.3.1...v1.4.0 276 | [1.3.1]: https://github.com/koopjs/featureservice/compare/v1.3.0...v1.3.1 277 | [1.3.0]: https://github.com/koopjs/featureservice/compare/v1.2.7...v1.3.0 278 | [1.2.7]: https://github.com/koopjs/featureservice/compare/v1.2.6...v1.2.7 279 | [1.2.6]: https://github.com/koopjs/featureservice/compare/v1.2.5...v1.2.6 280 | [1.2.5]: https://github.com/koopjs/featureservice/compare/v1.2.4...v1.2.5 281 | [1.2.4]: https://github.com/koopjs/featureservice/compare/v1.2.3...v1.2.4 282 | [1.2.3]: https://github.com/koopjs/featureservice/compare/v1.2.2...v1.2.3 283 | [1.2.2]: https://github.com/koopjs/featureservice/compare/v1.2.1...v1.2.2 284 | [1.2.1]: https://github.com/koopjs/featureservice/compare/v1.2.0...v1.2.1 285 | [1.2.0]: https://github.com/koopjs/featureservice/compare/v1.1.1...v1.2.0 286 | [1.1.1]: https://github.com/koopjs/featureservice/compare/v1.1.0...v1.1.1 287 | [1.1.0]: https://github.com/koopjs/featureservice/compare/v1.0.0...v1.1.0 288 | [1.0.0]: https://github.com/koopjs/featureservice/compare/v0.2.0...v1.0.0 289 | [0.2.0]: https://github.com/koopjs/featureservice/compare/v0.1.0...v0.2.0 290 | [0.1.0]: https://github.com/koopjs/featureservice/compare/v0.0.4...v0.1.0 291 | [0.0.4]: https://github.com/koopjs/featureservice/compare/v0.0.3...v0.0.4 292 | [0.0.3]: https://github.com/koopjs/featureservice/compare/v0.0.2...v0.0.3 293 | [0.0.2]: https://github.com/koopjs/featureservice/compare/v0.0.1...v0.0.2 294 | [0.0.1]: https://github.com/koopjs/featureservice/releases/tag/v0.0.1 295 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Esri 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # featureservice 2 | 3 | *Get all features from an Esri Feature Service* 4 | 5 | [![npm][npm-image]][npm-url] 6 | [![travis][travis-image]][travis-url] 7 | [![Greenkeeper badge][greenkeeper-image]][greenkeeper-url] 8 | 9 | [npm-image]: https://img.shields.io/npm/v/featureservice.svg?style=flat-square 10 | [npm-url]: https://www.npmjs.com/package/featureservice 11 | [travis-image]: https://img.shields.io/travis/koopjs/featureservice.svg?style=flat-square 12 | [travis-url]: https://travis-ci.org/koopjs/featureservice 13 | [greenkeeper-image]: https://badges.greenkeeper.io/koopjs/featureservice.svg 14 | [greenkeeper-url]: https://greenkeeper.io/ 15 | 16 | A little module that extracts every feature from an Esri Feature Service. The real power in this module is that it's designed to page over a service and extract every single feature no matter what ArcGIS Server version the data is hosted on. 17 | 18 | ## Install 19 | 20 | ``` 21 | npm install featureservice 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```javascript 27 | var FeatureService = require('featureservice') 28 | 29 | // a url to a feature service 30 | var url = 'http://....../FeatureServer/0' 31 | 32 | var service = new FeatureService(url, options) 33 | service.pages(function (err, pages) { 34 | /* will give you links to all pages of data in the service*/ 35 | }) 36 | ``` 37 | 38 | ### Options 39 | An object passed as the second parameter when initializing a service 40 | - layer: the layer index to use 41 | - size: the maximum page size when requesting features 42 | - concurrency: the maximum concurrency for requesting features from a single server 43 | - timeOut: the amount of time to wait with no response before cancelling a request 44 | - logger: An object with a log method that takes a level and a message e.g. a Winston instance 45 | 46 | ### API 47 | 48 | #### info(callback) 49 | Get the information describing the service itself 50 | 51 | #### layerIds(callback) 52 | Get all the ids in a feature service layer 53 | 54 | #### layerInfo(callback) 55 | Get the json metadata for a service layer 56 | 57 | #### statistics(field, stats, callback) 58 | Get statistics for a field and an array of stats. 59 | 60 | ```javascript 61 | service.statistics('id', ['min', 'max'], function (err, stats) { 62 | console.log(stats.features) 63 | }) 64 | ``` 65 | 66 | #### pages(callback) 67 | Returns an array of page urls that would get every feature in the service 68 | 69 | ### Browser 70 | 71 | A browser ready build of this module is in `dist/featureservice.min.js`. 72 | 73 | #### Example 74 | 75 | ```html 76 | 77 | 78 | 84 | 85 | ``` 86 | 87 | ## License 88 | 89 | [Apache 2.0](LICENSE) 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var queue = require('async').queue 2 | var Utils = require('./lib/utils.js') 3 | 4 | /** 5 | * Feature Service constructor. Requires a URL. 6 | * Exposes a pageQueue that can take an array of page URLs. 7 | * The pageQueue will report back each page of features as they return. 8 | * 9 | * @class 10 | * @param {string} url - address of feature service 11 | * @param {object} options - layer (default: 0) 12 | */ 13 | var FeatureService = function (url, options) { 14 | // catch omission of `new` keyword 15 | if (!(this instanceof FeatureService)) { 16 | return new FeatureService(url, options) 17 | } 18 | var service = Utils.parseUrl(url) 19 | this.hosted = service.hosted 20 | this.server = service.server 21 | this.options = options || {} 22 | if (this.options.layer || this.options.layer === 0) this.options.layer = Utils.sanitizeLayer(this.options.layer) 23 | this.options.size = this.options.size || 5000 24 | this.options.backoff = this.options.backoff || 1000 25 | this.options.timeOut = this.options.timeOut || (1.5 * 60 * 1000) 26 | this.layer = this.options.layer || service.layer || 0 27 | this.outSR = this.options.outSR || 4326 28 | 29 | this.logger = this.options.logger 30 | 31 | this._request = require('xhr-request') 32 | 33 | // an async for requesting pages of data 34 | this.pageQueue = queue(this._requestFeatures.bind(this), this.options.concurrency || 4) 35 | } 36 | 37 | /** 38 | * Wraps logging functionality so a logger can be passed in 39 | * 40 | * @param {string} level - the log level to use 41 | * @param {string} message - the message to log 42 | */ 43 | FeatureService.prototype.log = function (level, message) { 44 | if (this.logger && this.logger.log) return this.logger.log(level, message) 45 | this._console(level, message) 46 | } 47 | 48 | /** 49 | * Wraps console logging to make it testable 50 | * 51 | * @param {string} level - the log level to use 52 | * @param {string} message - the message to log 53 | */ 54 | FeatureService.prototype._console = function (level, message) { 55 | switch (level) { 56 | case 'info': 57 | console.info(message) 58 | break 59 | case 'warn': 60 | console.warn(message) 61 | break 62 | case 'error': 63 | console.error(message) 64 | break 65 | default: 66 | console.log(message) 67 | } 68 | } 69 | 70 | /** 71 | * Wrap the request.get method for easier testing 72 | * @param {string} url 73 | * @param {function} callback 74 | */ 75 | // TODO combine this with _requestFeatures 76 | FeatureService.prototype.request = function (url, callback) { 77 | var json 78 | // to ensure things are encoded just right for ArcGIS 79 | var encoded = encodeURI(decodeURI(url)) 80 | var options = { 81 | timeout: this.options.timeOut, 82 | headers: { 83 | 'user-agent': 'Featureservices-Node' 84 | } 85 | 86 | } 87 | this._request(encoded, options, function (err, data, res) { 88 | if (err) { 89 | if (err.code === 'ESOCKETTIMEDOUT') err.code = 504 90 | return callback(err) 91 | } 92 | try { 93 | json = JSON.parse(data) 94 | } catch (err) { 95 | // sometimes we get html or plain strings back 96 | var pattern = new RegExp(/[^{\[]/) // eslint-disable-line 97 | if (data.slice(0, 1).match(pattern)) { 98 | return callback(new Error('Received HTML or plain text when expecting JSON')) 99 | } else if (/\n(\n|.|\s)*$/.test(data)) { 100 | try { 101 | json = JSON.parse(data.replace(/\n(\n|.|\s)*$/, '')) 102 | return callback(null, json) 103 | } catch (e) { 104 | return callback(new Error('Failed to parse server response')) 105 | } 106 | } 107 | return callback(new Error('Failed to parse server response')) 108 | } 109 | callback(null, json) 110 | }) 111 | } 112 | 113 | /** 114 | * Builds a url for querying the min/max values of the object id 115 | * 116 | * @param {string} field - the name of a field to build a stat request for 117 | * @returns {string} url 118 | */ 119 | FeatureService.prototype._statsUrl = function (field, stats) { 120 | field = field || this.options.objectIdField 121 | var json = [] 122 | 123 | stats.forEach(function (stat) { 124 | json.push({ 125 | 'statisticType': stat, 126 | 'onStatisticField': field, 127 | 'outStatisticFieldName': stat + '_' + field 128 | }) 129 | }) 130 | 131 | return this.server + '/' + this.layer + '/query?f=json&outFields=&outStatistics=' + JSON.stringify(json) 132 | } 133 | 134 | /** 135 | * Gets the feature service info 136 | * @param {string} field - the name of a field to build a stat request for 137 | * @param {array} stats - an array of stats to request: ['min', 'max', 'avg', 'stddev', 'count'] 138 | */ 139 | FeatureService.prototype.statistics = function (field, stats, callback) { 140 | var url = this._statsUrl(field, stats) 141 | this.request(url, function (err, json) { 142 | if (err || json.error) { 143 | if (!json) json = {error: {}} 144 | var error = new Error('Request for statistics failed') 145 | error.timestamp = new Date() 146 | error.code = json.error.code || 500 147 | error.body = err || json.error 148 | error.url = url 149 | return callback(error) 150 | } 151 | callback(null, json) 152 | }) 153 | } 154 | 155 | /** 156 | * Gets the feature service info 157 | * @param {function} callback - called when the service info comes back 158 | */ 159 | FeatureService.prototype.info = function (callback) { 160 | if (typeof callback === 'undefined') return this._info 161 | if (this._info) return callback(null, this._info) 162 | var url = this.server + '?f=json' 163 | this.request(url, function (err, json) { 164 | /** 165 | * returns error on three conditions: 166 | * 1. err is present 167 | * 2. missing response json 168 | * 3. error in response json 169 | */ 170 | if (err || !json || json.error) { 171 | if (!json) json = {error: {}} 172 | var error = new Error('Request for service information failed') 173 | error.timestamp = new Date() 174 | error.url = url 175 | error.code = json.error.code || 500 176 | error.body = json.error 177 | 178 | return callback(error) 179 | } 180 | this._info = json 181 | json.url = url 182 | callback(null, json) 183 | }) 184 | } 185 | 186 | /** 187 | * Gets the feature service layer info 188 | * @param {function} callback - called when the layer info comes back 189 | */ 190 | FeatureService.prototype.layerInfo = function (callback) { 191 | // used saved version if available 192 | if (typeof callback === 'undefined') return this._layerInfo 193 | if (this._layerInfo) return callback(null, this._layerInfo) 194 | 195 | var url = this.server + '/' + this.layer + '?f=json' 196 | 197 | this.request(url, function (err, json) { 198 | /** 199 | * returns error on three conditions: 200 | * 1. err is present 201 | * 2. missing response json 202 | * 3. error in response json 203 | */ 204 | if (err || !json || json.error) { 205 | if (!json) json = {error: {}} 206 | var error = new Error('Request for layer information failed') 207 | error.timestamp = new Date() 208 | error.url = url 209 | error.code = json.error.code || 500 210 | error.body = json.error 211 | 212 | return callback(error) 213 | } 214 | 215 | json.url = url 216 | callback(null, json) 217 | }) 218 | } 219 | 220 | /** 221 | * Gets the objectID field from the service info 222 | @param {object} info the feature layer metadata 223 | @returns {string} service's object id field 224 | */ 225 | FeatureService.prototype.getObjectIdField = function (info) { 226 | var oid 227 | if (!info.fields) return false 228 | if (info.objectIdField) return info.objectIdField 229 | info.fields.some(function (field) { 230 | if (field.type === 'esriFieldTypeOID') { 231 | oid = field.name 232 | return true 233 | } 234 | }) 235 | 236 | return oid 237 | } 238 | 239 | /** 240 | * Gets the feature service object ids for pagination 241 | * @param {object} callback - called when the service info comes back 242 | */ 243 | FeatureService.prototype.layerIds = function (callback) { 244 | var url = this.server + '/' + this.layer + '/query?where=1=1&returnIdsOnly=true&f=json' 245 | this.request(url, function (err, json) { 246 | if (err || !json.objectIds) { 247 | if (!json) json = {error: {}} 248 | var error = new Error('Request for object IDs failed') 249 | error.timestamp = new Date() 250 | error.code = json.error.code || 500 251 | error.url = url 252 | error.body = err || json.error 253 | 254 | return callback(error) 255 | } 256 | // TODO: is this really necessary 257 | json.objectIds.sort(function (a, b) { return a - b }) 258 | callback(null, json.objectIds) 259 | }) 260 | } 261 | 262 | /** 263 | * Count of every single feature in the service 264 | * @param {object} callback - called when the service info comes back 265 | */ 266 | FeatureService.prototype.featureCount = function (callback) { 267 | var countUrl = this.server + '/' + (this.layer || 0) 268 | countUrl += '/query?where=1=1&returnCountOnly=true&f=json' 269 | 270 | this.request(countUrl, function (err, json) { 271 | if (err || json.error) { 272 | // init empty json error so we can handle building the error in one logic stream 273 | if (!json) json = {error: {}} 274 | var error = new Error('Request for feature count failed') 275 | error.timestamp = new Date() 276 | error.code = json.error.code || 500 277 | error.url = countUrl 278 | error.body = err || json.error 279 | 280 | return callback(error) 281 | } 282 | 283 | callback(null, json) 284 | }) 285 | } 286 | 287 | /** 288 | * Gets and derives layer metadata from two sources 289 | * @param {function} callback - called with an error or a metadata object 290 | */ 291 | FeatureService.prototype.metadata = function (callback) { 292 | if (typeof callback === 'undefined') return this._metadata 293 | if (this._metadata) return callback(null, this._metadata) 294 | 295 | this.layerInfo(function (err, layer) { 296 | if (err) { 297 | err.message = 'Unable to get layer metadata: ' + err.message 298 | return callback(err) 299 | } 300 | this._layerInfo = layer 301 | var oid = this.getObjectIdField(layer) 302 | var size = layer.maxRecordCount 303 | 304 | // TODO flatten this 305 | var metadata = {layer: layer, oid: oid, size: size} 306 | 307 | // 10.0 servers don't support count requests 308 | // they also do not show current version on the layer 309 | if (!layer.currentVersion) return callback(null, metadata) 310 | 311 | this.featureCount(function (err, json) { 312 | if (err) return callback(err) 313 | if (json.count < 1) return callback(new Error('Service returned count of 0')) 314 | metadata.count = json.count 315 | this._metadata = metadata 316 | callback(null, metadata) 317 | }) 318 | }.bind(this)) 319 | } 320 | 321 | /** 322 | * Build an array pages that will cover every feature in the service 323 | * @param {function} callback - called when the service info comes back 324 | */ 325 | FeatureService.prototype.pages = function (callback) { 326 | this.metadata(function (err, meta) { 327 | if (err) return callback(err) 328 | if (meta.count < meta.layer.maxRecordCount && meta.count < this.options.size) return callback(null, singlePage(this.server, this.layer, this.outSR)) 329 | this.concurrency = this.options.concurrency || Utils.setConcurrency(this.hosted, meta.layer.geometryType) 330 | this.maxConcurrency = this.concurrency 331 | this.pageQueue.concurrency = this.concurrency 332 | var size = Math.min(parseInt(meta.size, 10), 1000) || 1000 333 | // restrict page size to the passed in maximum 334 | if (size > 5000) size = this.options.size 335 | 336 | var layer = meta.layer 337 | var nPages = Math.ceil(meta.count / size) 338 | 339 | // if the service supports paging, we can use offset to build pages 340 | var canPage = layer.advancedQueryCapabilities && layer.advancedQueryCapabilities.supportsPagination 341 | if (canPage) { 342 | // Test pagination by requesting features from second page - supportsPagination has not been reliable 343 | var url = this.server + '/0/query?f=json&where=1=1&outFields=*&resultOffset=' + size + '&resultRecordCount=' + size 344 | this.request(url, function (err, json) { 345 | if (err) return callback(err) 346 | // If json features were returned, assume pagination works; otherwise use paging alternatives 347 | if (json && json.features && json.features.length > 0) { 348 | return callback(null, this._offsetPages(nPages, size)) 349 | } else { 350 | this.pagingAlternatives(meta, size, callback) 351 | } 352 | }.bind(this)) 353 | } else { 354 | this.pagingAlternatives(meta, size, callback) 355 | } 356 | }.bind(this)) 357 | } 358 | 359 | /** 360 | * Build pages when pagination is not supported 361 | * @param {function} callback - called when the service info comes back 362 | */ 363 | FeatureService.prototype.pagingAlternatives = function (meta, size, callback) { 364 | if (!meta.oid) return callback(new Error('ObjectID type field not found, unable to page')) 365 | this.options.objectIdField = meta.oid 366 | // if the service supports statistics, we can request the maximum and minimum id to build pages 367 | if (meta.layer.supportsStatistics) { 368 | this.getObjectIdRange(meta.oid, function (err, stats) { 369 | // if this worked then we can pagination using where clauses 370 | if (!err) return callback(null, this._rangePages(stats, size)) 371 | // if it failed, try to request all the ids and split them into pages 372 | this.layerIds(function (err, ids) { 373 | // either this works or we give up 374 | if (err) return callback(err) 375 | return callback(null, this._idPages(ids, size)) 376 | }.bind(this)) 377 | }.bind(this)) 378 | } else { 379 | // this is the last thing we can try 380 | this.layerIds(function (err, ids) { 381 | if (err) return callback(err) 382 | callback(null, this._idPages(ids, size)) 383 | }.bind(this)) 384 | } 385 | } 386 | 387 | /** 388 | * Handle feature server request where total number of features can be acquired in a single page 389 | * @param {*} server 390 | * @param {*} layer 391 | * @param {integer} outSR - wkid of output spatial reference 392 | */ 393 | function singlePage (server, layer, outSR = 4326) { 394 | return [{req: [server, '/', layer, '/query?where=1=1&returnGeometry=true&returnZ=true&outFields=*&outSR=' + outSR + '&f=json'].join('')}] 395 | } 396 | 397 | /** 398 | * Get the max and min object id 399 | * @param {object} meta - layer metadata, holds information needed to request oid stats 400 | * @param {function} callback - returns with an error or objectID stats 401 | */ 402 | FeatureService.prototype.getObjectIdRange = function (oidField, callback) { 403 | this.statistics(oidField, ['min', 'max'], function (err, statResponse) { 404 | // TODO this is handled elsewhere now so move it 405 | if (err) return callback(err) 406 | var eMsg = 'Response from statistics was invalid' 407 | var stats 408 | try { 409 | stats = findMinAndMax(statResponse) 410 | } catch (e) { 411 | return callback(new Error(eMsg)) 412 | } 413 | if (!stats.min > 0 && !stats.max > 0) return callback(new Error(eMsg)) 414 | callback(null, stats) 415 | }) 416 | } 417 | 418 | function findMinAndMax (statResponse) { 419 | var attributes = statResponse.features[0].attributes 420 | var values = Object.keys(attributes).map(function (key) { 421 | return attributes[key] 422 | }) 423 | var minMax = {} 424 | minMax.min = values[0] < values[1] ? values[0] : values[1] 425 | minMax.max = values[1] > values[0] ? values[1] : values[0] 426 | return minMax 427 | } 428 | 429 | /** 430 | * build result Offset based page requests 431 | * these pages use Server's built in paging via resultOffset and resultRecordCount 432 | * @param {integer} pages - the number of pages we'll create 433 | * @param {integer} size - the max number of features per page 434 | * @returns {object} reqs - contains all the pages for extracting features 435 | */ 436 | FeatureService.prototype._offsetPages = function (pages, size) { 437 | var reqs = [] 438 | var resultOffset 439 | var url = this.server 440 | 441 | for (var i = 0; i < pages; i++) { 442 | resultOffset = i * size 443 | var pageUrl = url + '/' + this.layer + '/query?outSR=' + this.outSR + '&f=json&outFields=*&where=1=1' 444 | if (pages === 1) return [{req: pageUrl + '&geometry=&returnGeometry=true&returnZ=true&geometryPrecision='}] 445 | pageUrl += '&resultOffset=' + resultOffset 446 | pageUrl += '&resultRecordCount=' + size 447 | pageUrl += '&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=' 448 | reqs.push({req: pageUrl}) 449 | } 450 | 451 | return reqs 452 | } 453 | 454 | /** 455 | * build `id` query based page requests 456 | * these pages use object ids in URLs directly 457 | * @param {array} ids - an array of each object id in the service 458 | * @param {integer} size - the max record count for each page 459 | * @returns {object} reqs - contains all the pages for extracting features 460 | */ 461 | FeatureService.prototype._idPages = function (ids, size) { 462 | var reqs = [] 463 | var oidField = this.options.objectIdField || 'objectId' 464 | var pages = (ids.length / size) 465 | 466 | for (var i = 0; i < pages + 1; i++) { 467 | var pageIds = ids.splice(0, size) 468 | if (pageIds.length) { 469 | var pageMin = pageIds[0] 470 | var pageMax = pageIds.pop() 471 | var where = [oidField, ' >= ', pageMin, ' AND ', oidField, '<=', pageMax].join('') 472 | var pageUrl = this.server + '/' + (this.layer) + '/query?outSR=' + this.outSR + '&where=' + where + '&f=json&outFields=*' 473 | pageUrl += '&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=10' 474 | reqs.push({req: pageUrl}) 475 | } 476 | } 477 | 478 | return reqs 479 | } 480 | 481 | /** 482 | * build object id query based page requests 483 | * these pages use object ids in where clauses via < and > 484 | * you could call this objectId queries 485 | * @param {object} stats - contains the max and min object id 486 | * @param {integer} size - the size of records to include in each page 487 | * @returns {object} reqs - contains all the pages for extracting features 488 | */ 489 | FeatureService.prototype._rangePages = function (stats, size) { 490 | var reqs = [] 491 | var pageUrl 492 | var pageMax 493 | var pageMin 494 | var where 495 | var objId = this.options.objectIdField 496 | 497 | var url = this.server 498 | var pages = Math.max((stats.max === size) ? stats.max : Math.ceil((stats.max - stats.min) / size), 1) 499 | 500 | for (var i = 0; i < pages; i++) { 501 | // there is a bug in server where queries fail if the max value queried is higher than the actual max 502 | // so if this is the last page, then set the max to be the maxOID 503 | if (i === pages - 1) { 504 | pageMax = stats.max 505 | } else { 506 | pageMax = stats.min + (size * (i + 1)) - 1 507 | } 508 | pageMin = stats.min + (size * i) 509 | where = [objId, '>=', pageMin, '+AND+', objId, '<=', pageMax].join('') 510 | pageUrl = url + '/' + (this.layer || 0) + '/query?outSR=' + this.outSR + '&where=' + where + '&f=json&outFields=*' 511 | pageUrl += '&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=' 512 | reqs.push({req: pageUrl}) 513 | } 514 | 515 | return reqs 516 | } 517 | 518 | /** 519 | * Requests a page of features 520 | * @param {object} task - a task object with a "req" property 521 | * @param {function} callback 522 | */ 523 | FeatureService.prototype._requestFeatures = function (task, cb) { 524 | var self = this 525 | 526 | this.request(task.req, function (err, json) { 527 | if (err) return self._catchErrors(task, err, task.req, cb) 528 | if (!json || json.error) { 529 | if (!json) json = {error: {}} 530 | var error = new Error('Request for a page of features failed') 531 | error.timestamp = new Date() 532 | error.body = json.error 533 | error.code = json.error.code || 500 534 | return self._catchErrors(task, error, task.req, cb) 535 | } 536 | self._throttleQueue() 537 | cb(null, json) 538 | }) 539 | } 540 | 541 | /* Catches an errors during paging and handles retry logic 542 | * @param {object} task - the currently executing job 543 | * @param {object} e - the error in application logic or from a failed request to a server 544 | * @param {string} url - the url of the last request for pages 545 | * @param {function} cb - callback passed through to the abort paging function 546 | */ 547 | FeatureService.prototype._catchErrors = function (task, error, url, cb) { 548 | this._throttleQueue(error) 549 | // be defensive in case there was no json payload 550 | error.body = error.body || {} 551 | // set the error code from the json payload if the error doesn't have one already 552 | if (!error.code) error.code = error.body.code 553 | error.url = url 554 | if (task.retry && task.retry === 3) return this._abortPaging(error, cb) 555 | // initiate the count or increment it 556 | if (!task.retry) { 557 | task.retry = 1 558 | } else { 559 | task.retry++ 560 | } 561 | 562 | this.log('info', 'Re-requesting page ' + task.req + ' attempt ' + task.retry) 563 | 564 | setTimeout(function () { 565 | this._requestFeatures(task, cb) 566 | }.bind(this), task.retry * this.options.backoff) 567 | } 568 | 569 | FeatureService.prototype._throttleQueue = function (fail) { 570 | if (fail) this.concurrency -= 0.5 571 | else this.concurrency += 0.1 572 | 573 | if (this.concurrency > this.maxConcurrency) this.concurrency = this.maxConcurrency 574 | 575 | this.pageQueue.concurrency = this.concurrency >= 1 ? Math.floor(this.concurrency) : 1 576 | } 577 | 578 | /** 579 | * Aborts the request queue by emptying all queued up tasks 580 | * @param {object} error - error payload to send back to the original requestor 581 | * @param {function} callback - calls back with the error payload 582 | */ 583 | FeatureService.prototype._abortPaging = function (error, callback) { 584 | this.pageQueue.kill() 585 | error.message = 'Paging aborted: ' + error.message 586 | callback(error) 587 | } 588 | 589 | module.exports = FeatureService 590 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * Given a service url and a geometry type, determines a default concurrency for requests 4 | * 5 | * @param {boolean} hosted - whether or not the service is hosted on ArcGIS Online 6 | * @param {string} geomType - the geometry type of the features in the service 7 | * @return {integer} the suggested concurrency 8 | */ 9 | setConcurrency: function (hosted, geomType) { 10 | var naieve = hosted ? 16 : 4 11 | if (!geomType) return naieve 12 | var concurrency = geomType.match(/point/i) ? naieve : naieve / 4 13 | return Math.floor(concurrency) 14 | }, 15 | /** 16 | * Parsed the layer and server from a feature service url 17 | * @param {string} url - a link to a feature service 18 | * @return {object} contains the layer, the server and whether or not the server is hosted 19 | */ 20 | parseUrl: function (url) { 21 | // @todo: use the URL class once pre-node 6 has been deprecated 22 | var match = url.match(/^(.+?\/(?:feature|map)server)(?:\/(\d+))?/i) 23 | 24 | if (null === match) { 25 | throw new TypeError('unable to parse ' + url + ' as a mapserver or featureserver with optional layer') 26 | } 27 | 28 | return { 29 | layer: match[2], 30 | server: match[1], 31 | hosted: /services(\d)?(qa|dev)?.arcgis.com/.test(url) 32 | } 33 | }, 34 | /** 35 | * Strip characters off a layer that don't belong 36 | * @param {string} raw - a raw layer options 37 | * @return {string} the layer index 38 | */ 39 | sanitizeLayer: function (raw) { 40 | if (typeof raw === 'number') return raw 41 | else if (typeof raw !== 'string') return undefined 42 | 43 | var match = raw.match(/\/?(\d+)/) 44 | return match && match[1] ? match[1] : undefined 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "featureservice", 3 | "description": "Get all features from an Esri Feature Service", 4 | "version": "1.6.2", 5 | "author": "Chris Helm", 6 | "bugs": { 7 | "url": "https://github.com/koopjs/featureservice/issues" 8 | }, 9 | "contributors": [ 10 | "Daniel Fenton" 11 | ], 12 | "dependencies": { 13 | "async": "^2.1.5", 14 | "xhr-request": "^1.0.1" 15 | }, 16 | "devDependencies": { 17 | "browserify": "^14.1.0", 18 | "http-browserify": "^1.7.0", 19 | "https-browserify": "1.0.0", 20 | "lodash": "^4.0.0", 21 | "minifyify": "^7.0.3", 22 | "nock": "^9.0.9", 23 | "sinon": "^3.2.1", 24 | "standard": "^10.0.0", 25 | "tap-spec": "^4.1.2", 26 | "tape": "^4.9.1", 27 | "zlib-browserify": "0.0.3" 28 | }, 29 | "homepage": "https://github.com/koopjs/featureservice", 30 | "keywords": [ 31 | "esri", 32 | "featureservice", 33 | "gis" 34 | ], 35 | "license": "ISC", 36 | "main": "index.js", 37 | "repository": { 38 | "type": "git", 39 | "url": "https://github.com/koopjs/featureservice.git" 40 | }, 41 | "scripts": { 42 | "build": "browserify -d index.js -s FeatureService -p [minifyify --map featureservice.map.json --output dist/featureservice.map.json] > dist/featureservice.min.js", 43 | "test": "standard index.js && tape test/*.js | tap-spec" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/fixtures/count.json: -------------------------------------------------------------------------------- 1 | {"count":82} -------------------------------------------------------------------------------- /test/fixtures/countFailedPaging.json: -------------------------------------------------------------------------------- 1 | { 2 | "count": 15592 3 | } -------------------------------------------------------------------------------- /test/fixtures/countNoPaging.json: -------------------------------------------------------------------------------- 1 | { 2 | "count": 88 3 | } -------------------------------------------------------------------------------- /test/fixtures/countPaging.json: -------------------------------------------------------------------------------- 1 | {"count":15592} -------------------------------------------------------------------------------- /test/fixtures/countStatsFail.json: -------------------------------------------------------------------------------- 1 | {"count":3355} -------------------------------------------------------------------------------- /test/fixtures/features-empty.json: -------------------------------------------------------------------------------- 1 | {"objectIdFieldName":"FID","globalIdFieldName":"","geometryType":"esriGeometryPoint","spatialReference":{"wkid":102100,"latestWkid":3857},"fields":[],"exceededTransferLimit":true,"features":[]} -------------------------------------------------------------------------------- /test/fixtures/features.json: -------------------------------------------------------------------------------- 1 | {"objectIdFieldName":"FID","globalIdFieldName":"","geometryType":"esriGeometryPoint","spatialReference":{"wkid":102100,"latestWkid":3857},"fields":[],"exceededTransferLimit":true,"features":[{"attributes":{},"geometry":{"x":-8572256.9081608,"y":4708222.9619573336}}]} -------------------------------------------------------------------------------- /test/fixtures/hostedLayerInfo.json: -------------------------------------------------------------------------------- 1 | {"currentVersion":10.3,"id":0,"name":"download_test","type":"Feature Layer","serviceItemId":"c6bfd157874f483ba85eacad852aea23","displayField":"","description":"","copyrightText":"","defaultVisibility":true,"editingInfo":{"lastEditDate":1447869075942},"relationships":[],"isDataVersioned":false,"supportsCalculate":true,"supportsAttachmentsByUploadId":true,"supportsRollbackOnFailureParameter":true,"supportsStatistics":true,"supportsAdvancedQueries":true,"supportsValidateSql":true,"supportsCoordinatesQuantization":true,"supportsApplyEditsWithGlobalIds":false,"advancedQueryCapabilities":{"supportsPagination":true,"supportsQueryWithDistance":true,"supportsReturningQueryExtent":true,"supportsStatistics":true,"supportsOrderBy":true,"supportsDistinct":true,"supportsQueryWithResultType":true,"supportsSqlExpression":true,"supportsReturningGeometryCentroid":false},"useStandardizedQueries":false,"geometryType":"esriGeometryPoint","minScale":9244649,"maxScale":0,"extent":{"xmin":-9796115.1898080744,"ymin":4736005.8538175914,"xmax":-9695927.6480941288,"ymax":4865942.279503176,"spatialReference":{"wkid":102100,"latestWkid":3857}},"drawingInfo":{"renderer":{"type":"simple","symbol":{"type":"esriPMS","url":"RedSphere.png","imageData":"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQBQYWludC5ORVQgdjMuNS4xTuc4+QAAB3VJREFUeF7tmPlTlEcexnve94U5mANQbgQSbgiHXHINlxpRIBpRI6wHorLERUmIisKCQWM8cqigESVQS1Kx1piNi4mW2YpbcZONrilE140RCTcy3DDAcL/zbJP8CYPDL+9Ufau7uqb7eZ7P+/a8PS8hwkcgIBAQCAgEBAICAYGAQEAgIBAQCAgEBAICAYGAQEAgIBAQCDx/AoowKXFMUhD3lQrioZaQRVRS+fxl51eBTZUTdZ41U1Rox13/0JF9csGJ05Qv4jSz/YPWohtvLmSKN5iTGGqTm1+rc6weICOBRbZs1UVnrv87T1PUeovxyNsUP9P6n5cpHtCxu24cbrmwKLdj+osWiqrVKhI0xzbmZ7m1SpJ+1pFpvE2DPvGTomOxAoNLLKGLscZYvB10cbYYjrJCb7A5mrxleOBqim+cWJRakZY0JfnD/LieI9V1MrKtwokbrAtU4Vm0A3TJnphJD4B+RxD0u0LA7w7FTE4oprOCMbklEGNrfdGf4IqnQTb4wc0MFTYibZqM7JgjO8ZdJkpMln/sKu16pHZGb7IfptIWg389DPp9kcChWODoMuDdBOhL1JgpisbUvghM7AqFbtNiaFP80RLnhbuBdqi0N+1dbUpWGde9gWpuhFi95yL7sS7BA93JAb+Fn8mh4QujgPeTgb9kAZf3Apd2A+fXQ38yHjOHozB1IAJjOSEY2RSIwVUv4dd4X9wJccGHNrJ7CYQ4GGjLeNNfM+dyvgpzQstKf3pbB2A6m97uBRE0/Ergcxr8hyqg7hrwn0vAtRIKIRX6Y2pMl0RhIj8co9nBGFrvh55l3ngU7YObng7IVnFvGS+BYUpmHziY/Ls2zgP9SX50by/G9N5w6I+ogYvpwK1SoOlHQNsGfWcd9Peqof88B/rTyzF9hAIopAByQzC0JQB9ST5oVnvhnt+LOGsprvUhxNIwa0aY7cGR6Cp7tr8+whkjawIxkRWC6YJI6N+lAKq3Qf/Tx+B77oGfaQc/8hB8w2Xwtw9Bf3kzZspXY/JIDEbfpAB2BKLvVV90Jvjgoac9vpRxE8kciTVCBMMkNirJ7k/tRHyjtxwjKV4Yp3t/6s+R4E+/DH3N6+BrS8E314Dvvg2+/Sb4hxfBf5sP/up2TF3ZhonK1zD6dhwGdwail26DzqgX8MRKiq9ZBpkSkmeYOyPM3m9Jjl+1Z9D8AgNtlAq6bZ70qsZi+q+bwV/7I/hbB8D/dAr8Axq89iz474p/G5++koHJy1sx/lkGdBc2YjA3HF0rHNHuboomuQj/5DgclIvOGCGCYRKFFuTMV7YUAD3VDQaLMfyqBcZORGPy01QKYSNm/rYV/Nd/Av9NHvgbueBrsjDzRQamKKDxT9Kgq1iLkbIUDOSHoiNcgnYHgnYZi+9ZExSbiSoMc2eE2flKcuJLa4KGRQz6/U0wlGaP0feiMH4uFpMXEjBVlYjp6lWY+SSZtim0kulYMiYuJEJXuhTDJ9UYPByOvoIwdCxfgE4bAo0Jh39xLAoVpMwIEQyTyFCQvGpLon9sJ0K3J4OBDDcMH1dj9FQsxkrjMPFRPCbOx2GyfLal9VEcxstioTulxjAFNfROJPqLl6Bnfyg6V7ugz5yBhuHwrZjBdiU5YJg7I8wOpifAKoVIW7uQ3rpOBH2b3ekVjYT2WCRG3o+mIGKgO0OrlIaebU/HYOQDNbQnojB4NJyGD0NPfjA0bwTRE6Q7hsUcWhkWN8yZqSQlWWGECAZLmJfJmbrvVSI8taK37xpbdB/wQW8xPee/8xIGjvlj8IQ/hk4G0JbWcX8MHPVDX4kveoq8ocn3xLM33NCZRcPHOGJYZIKfpQyq7JjHS6yJjcHujLHADgkpuC7h8F8zEVqXSNC2awE69lqhs8AamkO26HrbDt2H7dBVQov2NcW26CiwQtu+BWjdY4n2nZboTbfCmKcCnRyDO/YmyLPnDlHvjDH8G6zhS9/wlEnYR7X00fWrFYuWdVI0ZpuhcbcczW/R2qdAcz6t/bRov4mONeaaoYl+p22rHF0bVNAmKtBvweIXGxNcfFH8eNlC4m6wMWMusEnKpn5hyo48pj9gLe4SNG9QoGGLAk8z5XiaJUd99u8122/IpBA2K9BGg2vWWKAvRYVeLzEa7E1R422m2+MsSTem97nSYnfKyN6/mzATv7AUgqcMrUnmaFlLX3ysM0fj+t/b5lQLtK22QEfyAmiSLKFZpUJ7kBRPXKW4HqCYynWVHKSG2LkyZex1uO1mZM9lKem9Tx9jjY5iNEYo0bKMhn7ZAu0r6H5PpLXCAq0rKJClSjSGynE/QIkrQYqBPe6S2X+AJsY2Ped6iWZk6RlL0c2r5szofRsO9R5S1IfQLRCpQL1aifoYFerpsbkuTImaUJXuXIDiH6/Ys8vm3Mg8L2i20YqsO7fItKLcSXyn0kXccclVqv3MS6at9JU/Ox+ouns+SF6Z4cSupz7l8+z1ucs7LF1AQjOdxfGZzmx8Iu1TRcfnrioICAQEAgIBgYBAQCAgEBAICAQEAgIBgYBAQCAgEBAICAQEAv8H44b/6ZiGvGAAAAAASUVORK5CYII=","contentType":"image/png","width":15,"height":15}}},"allowGeometryUpdates":true,"hasAttachments":false,"htmlPopupType":"esriServerHTMLPopupTypeNone","hasM":false,"hasZ":false,"objectIdField":"FID","globalIdField":"","typeIdField":"","fields":[{"name":"FID","type":"esriFieldTypeInteger","actualType":"int","alias":"FID","sqlType":"sqlTypeInteger","nullable":false,"editable":false,"domain":null,"defaultValue":null},{"name":"rownum","type":"esriFieldTypeInteger","actualType":"int","alias":"rownum","sqlType":"sqlTypeInteger","nullable":true,"editable":true,"domain":null,"defaultValue":null},{"name":"latitude","type":"esriFieldTypeDouble","actualType":"float","alias":"latitude","sqlType":"sqlTypeFloat","nullable":true,"editable":true,"domain":null,"defaultValue":null},{"name":"longitude","type":"esriFieldTypeDouble","actualType":"float","alias":"longitude","sqlType":"sqlTypeFloat","nullable":true,"editable":true,"domain":null,"defaultValue":null},{"name":"quant_val","type":"esriFieldTypeInteger","actualType":"int","alias":"quant_val","sqlType":"sqlTypeInteger","nullable":true,"editable":true,"domain":null,"defaultValue":null},{"name":"expected_c","type":"esriFieldTypeString","actualType":"nvarchar","alias":"expected_c","sqlType":"sqlTypeNVarchar","length":80,"nullable":true,"editable":true,"domain":null,"defaultValue":null},{"name":"FID_1","type":"esriFieldTypeInteger","actualType":"int","alias":"FID_1","sqlType":"sqlTypeInteger","nullable":true,"editable":true,"domain":null,"defaultValue":null}],"indexes":[{"name":"PK__DOWNLOAD__C1BEA5A22547B608","fields":"FID","isAscending":true,"isUnique":true,"description":"clustered, unique, primary key"},{"name":"user_1374.DOWNLOAD_TEST_DOWNLOAD_TEST_Shape_sidx","fields":"Shape","isAscending":false,"isUnique":false,"description":"Shape Index"}],"types":[],"templates":[{"name":"New Feature","description":"","drawingTool":"esriFeatureEditToolPoint","prototype":{"attributes":{"rownum":null,"latitude":null,"longitude":null,"quant_val":null,"expected_c":null,"FID_1":null}}}],"supportedQueryFormats":"JSON","hasStaticData":true,"maxRecordCount":2000,"standardMaxRecordCount":32000,"tileMaxRecordCount":8000,"maxRecordCountFactor":1,"capabilities":"Query"} -------------------------------------------------------------------------------- /test/fixtures/ids10.0.json: -------------------------------------------------------------------------------- 1 | {"objectIdFieldName":"objectid","objectIds":[1,2,3,4,5,6]} -------------------------------------------------------------------------------- /test/fixtures/idsStatsFail.json: -------------------------------------------------------------------------------- 1 | {"objectIdFieldName":"ESRI_OID","objectIds":[43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,68,66,67,69,70,71,72,73,74,75,77,76,79,78,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,1172,1173,1174,1175,1176,1177,1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188,1189,1190,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,1216,1217,1218,1219,1220,1221,1222,1223,1224,1225,1226,1227,1228,1229,1230,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1287,1288,1289,1290,1291,1292,1293,1294,1295,1296,1297,1298,1299,1300,1301,1302,1303,1304,1305,1306,1307,1308,1309,1310,1311,1312,1313,1314,1315,1316,1317,1318,1319,1320,1321,1322,1323,1324,1325,1326,1327,1328,1329,1330,1331,1332,1333,1334,1335,1336,1337,1338,1339,1340,1341,1342,1343,1344,1345,1346,1347,1348,1349,1350,1351,1352,1353,1354,1355,1356,1357,1358,1359,1360,1361,1362,1363,1364,1365,1366,1367,1368,1369,1370,1371,1372,1373,1374,1375,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1476,1477,1478,1479,1480,1481,1482,1483,1484,1485,1486,1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,1515,1516,1517,1518,1519,1520,1521,1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1540,1541,1542,1543,1544,1545,1546,1547,1548,1549,1550,1551,1552,1553,1554,1555,1556,1557,1558,1559,1560,1561,1562,1563,1564,1565,1566,1567,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1580,1579,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,1596,1595,1597,1598,1599,1600,1601,1602,1603,1604,1605,1606,1607,1608,1609,1610,1611,1612,1613,1614,1615,1616,1617,1618,1619,1620,1621,1622,1623,1625,1624,1626,1627,1628,1629,1630,1631,1632,1633,1634,1635,1636,1637,1638,1639,1640,1641,1642,1643,1644,1645,1646,1647,1648,1649,1650,1651,1652,1653,1654,1655,1656,1657,1658,1659,1660,1661,1662,1663,1664,1665,1666,1667,1668,1669,1670,1671,1672,1673,1674,1675,1676,1677,1678,1679,1680,1681,1683,1682,1684,1685,1686,1687,1688,1689,1690,1691,1692,1693,1694,1695,1697,1696,1698,1699,1700,1701,1702,1703,1704,1705,1706,1707,1708,1709,1710,1711,1712,1713,1714,1715,1716,1717,1718,1719,1720,1721,1722,1723,1724,1725,1726,1727,1728,1729,1730,1731,1732,1733,1734,1735,1736,1737,1738,1739,1740,1741,1742,1743,1744,1745,1746,1747,1748,1749,1750,1751,1752,1753,1754,1756,1755,1757,1758,1759,1760,1761,1762,1763,1764,1765,1766,1767,1768,1769,1770,1771,1772,1773,1774,1775,1776,1777,1778,1779,1780,1781,1782,1783,1784,1785,1786,1787,1788,1789,1790,1791,1792,1793,1794,1795,1796,1797,1798,1799,1800,1801,1802,1803,1804,1806,1805,1807,1808,1809,1810,1811,1812,1813,1814,1816,1815,1818,1819,1817,1820,1821,1822,1823,1824,1825,1826,1827,1828,1829,1830,1831,1833,1832,1834,1835,1836,1837,1838,1839,1840,1841,1842,1843,1844,1845,1846,1847,1848,1849,1850,1851,1852,1853,1854,1855,1856,1857,1858,1859,1860,1862,1861,1863,1864,1865,1866,1867,1868,1869,1870,1871,1872,1873,1874,1875,1876,1877,1878,1879,1880,1881,1882,1883,1884,1885,1887,1886,1888,1889,1890,1891,1892,1893,1894,1895,1896,1897,1898,1899,1900,1901,1902,1903,1904,1905,1906,1907,1908,1909,1910,1911,1912,1913,1914,1915,1916,1917,1918,1919,1920,1921,1922,1923,1924,1925,1926,1927,1928,1929,1930,1931,1932,1933,1934,1935,1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1946,1947,1948,1950,1949,1951,1952,1953,1954,1955,1956,1957,1958,1959,1960,1961,1962,1963,1964,1965,1966,1967,1968,1969,1970,1971,1972,1973,1974,1975,1976,1977,1978,1979,1980,1981,1982,1983,1984,1985,1986,1987,1988,1989,1990,1991,1992,1993,1994,1995,1996,1997,1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2023,2024,2022,2025,2026,2027,2028,2029,2030,2031,2032,2033,2034,2035,2036,2037,2038,2039,2040,2041,2042,2043,2044,2045,2046,2047,2048,2049,2050,2051,2052,2053,2054,2055,2056,2057,2058,2059,2060,2061,2062,2063,2064,2065,2066,2067,2068,2069,2070,2072,2071,2073,2074,2075,2076,2077,2078,2079,2080,2081,2082,2083,2084,2085,2086,2087,2088,2089,2090,2091,2092,2093,2094,2095,2096,2097,2098,2099,2100,2101,2102,2103,2104,2105,2106,2107,2108,2109,2110,2111,2112,2113,2114,2115,2116,2117,2118,2119,2120,2121,2122,2123,2124,2125,2126,2127,2128,2129,2130,2131,2132,2133,2134,2135,2136,2137,2138,2139,2140,2141,2142,2143,2144,2145,2146,2148,2147,2149,2150,2151,2152,2153,2154,2155,2156,2157,2158,2159,2160,2161,2162,2163,2164,2165,2166,2167,2168,2169,2170,2171,2172,2173,2174,2175,2176,2177,2178,2179,2180,2181,2182,2183,2184,2185,2186,2187,2188,2189,2190,2191,2192,2193,2194,2195,2196,2197,2198,2199,2200,2201,2202,2203,2204,2205,2206,2207,2208,2209,2210,2211,2212,2213,2214,2215,2216,2217,2218,2220,2219,2221,2222,2223,2224,2225,2226,2227,2228,2229,2230,2231,2233,2232,2234,2235,2236,2237,2238,2239,2240,2241,2242,2243,2244,2245,2246,2247,2248,2249,2250,2251,2252,2253,2254,2255,2256,2257,2258,2259,2261,2260,2262,2263,2264,2265,2266,2267,2268,2269,2270,2271,2272,2273,2274,2275,2276,2277,2278,2279,2280,2281,2282,2283,2284,2285,2286,2287,2288,2289,2290,2291,2292,2293,2294,2295,2296,2297,2298,2299,2300,2301,2302,2303,2304,2305,2306,2307,2308,2309,2310,2311,2312,2314,2313,2315,2316,2317,2318,2319,2320,2321,2322,2323,2324,2325,2326,2327,2328,2329,2330,2331,2332,2333,2334,2335,2336,2337,2338,2339,2340,2341,2342,2343,2344,2345,2347,2346,2348,2349,2350,2351,2352,2353,2354,2355,2356,2357,2358,2359,2360,2361,2362,2363,2364,2365,2366,2367,2368,2369,2370,2371,2372,2373,2374,2375,2376,2377,2378,2379,2380,2381,2382,2383,2384,2385,2386,2387,2388,2389,2390,2391,2392,2393,2394,2395,2396,2397,2398,2399,2400,2401,2403,2402,2404,2405,2406,2408,2407,2409,2410,2411,2412,2413,2414,2415,2416,2417,2418,2419,2420,2421,2422,2423,2424,2425,2426,2427,2428,2429,2430,2431,2432,2433,2434,2435,2436,2437,2439,2438,2440,2441,2442,2443,2444,2445,2446,2447,2449,2448,2450,2451,2452,2453,2454,2455,2456,2457,2458,2459,2460,2461,2462,2463,2464,2465,2466,2467,2468,2469,2470,2471,2472,2473,2474,2475,2477,2476,2479,2478,2481,2480,2483,2482,2484,2485,2486,2488,2487,2489,2490,2491,2492,2493,2494,2495,2496,2497,2498,2499,2500,2501,2502,2503,2505,2504,2506,2507,2508,2509,2510,2511,2512,2513,2514,2515,2516,2517,2518,2519,2520,2521,2522,2523,2524,2525,2526,2527,2528,2529,2530,2531,2532,2534,2533,2535,2536,2537,2538,2539,2540,2541,2542,2543,2544,2545,2546,2547,2548,2549,2550,2551,2552,2553,2554,2555,2556,2557,2558,2559,2560,2561,2562,2563,2564,2566,2565,2567,2568,2569,2570,2571,2572,2573,2574,2575,2576,2577,2578,2579,2580,2581,2582,2583,2584,2585,2586,2587,2588,2589,2590,2591,2592,2593,2594,2595,2596,2597,2598,2599,2600,2601,2602,2603,2604,2605,2606,2607,2608,2609,2610,2611,2612,2613,2615,2614,2616,2617,2618,2619,2620,2621,2622,2623,2624,2625,2626,2627,2628,2629,2630,2631,2633,2632,2634,2635,2636,2637,2638,2639,2640,2641,2642,2643,2644,2645,2646,2647,2648,2649,2650,2651,2652,2653,2654,2655,2656,2657,2658,2659,2660,2661,2662,2663,2664,2665,2666,2667,2668,2669,2670,2671,2672,2673,2674,2675,2676,2677,2678,2679,2680,2681,2682,2683,2684,2685,2686,2687,2688,2689,2690,2691,2692,2693,2694,2695,2696,2697,2698,2699,2700,2701,2702,2703,2704,2705,2706,2707,2708,2709,2710,2711,2712,2713,2714,2715,2716,2717,2718,2719,2720,2721,2722,2723,2724,2725,2726,2727,2728,2729,2730,2731,2732,2733,2734,2735,2736,2737,2738,2739,2740,2741,2742,2743,2744,2745,2746,2747,2748,2749,2750,2751,2752,2753,2754,2755,2756,2757,2758,2759,2760,2761,2762,2763,2764,2765,2766,2767,2768,2769,2770,2771,2772,2773,2774,2775,2776,2777,2778,2779,2780,2781,2782,2783,2784,2785,2786,2787,2788,2789,2790,2791,2792,2793,2794,2795,2796,2797,2798,2799,2800,2801,2802,2803,2804,2805,2806,2807,2808,2809,2810,2811,2812,2813,2814,2815,2816,2817,2818,2819,2820,2821,2822,2823,2824,2825,2826,2827,2828,2829,2830,2831,2832,2833,2834,2835,2836,2837,2838,2839,2840,2841,2842,2843,2844,2845,2846,2847,2848,2849,2850,2851,2852,2853,2854,2855,2856,2857,2858,2859,2860,2861,2862,2863,2864,2865,2866,2867,2868,2869,2870,2871,2872,2873,2874,2875,2876,2877,2878,2879,2880,2881,2882,2883,2884,2885,2886,2887,2888,2889,2890,2891,2892,2893,2894,2895,2896,2897,2898,2899,2900,2901,2902,2903,2904,2905,2906,2907,2908,2909,2910,2911,2912,2913,2914,2915,2916,2917,2918,2919,2920,2921,2922,2923,2924,2925,2926,2927,2928,2929,2930,2931,2932,2933,2934,2935,2936,2937,2938,2939,2940,2941,2942,2943,2944,2945,2946,2947,2948,2949,2950,2951,2952,2953,2954,2955,2956,2957,2958,2959,2960,2961,2962,2963,2964,2965,2966,2967,2968,2969,2970,2971,2972,2973,2974,2975,2976,2977,2978,2979,2980,2981,2982,2983,2984,2985,2986,2987,2988,2989,2990,2991,2992,2993,2994,2995,2996,2997,2998,2999,3000,3001,3002,3003,3004,3005,3006,3007,3008,3009,3010,3011,3012,3013,3014,3015,3016,3017,3018,3019,3020,3021,3022,3023,3024,3025,3026,3027,3028,3029,3030,3031,3032,3033,3034,3035,3036,3037,3038,3039,3040,3041,3042,3043,3044,3045,3046,3047,3048,3049,3050,3051,3052,3053,3054,3055,3056,3057,3058,3059,3060,3061,3062,3063,3064,3065,3066,3067,3068,3069,3070,3071,3072,3073,3074,3075,3076,3077,3078,3079,3080,3081,3082,3083,3084,3085,3086,3087,3088,3089,3090,3091,3092,3093,3094,3095,3096,3097,3098,3099,3100,3101,3102,3103,3104,3105,3106,3107,3108,3109,3110,3111,3112,3113,3114,3115,3116,3117,3118,3119,3120,3121,3122,3123,3124,3125,3126,3127,3128,3129,3130,3131,3132,3133,3134,3135,3136,3137,3138,3139,3140,3141,3142,3143,3144,3145,3146,3147,3148,3149,3150,3151,3152,3153,3154,3155,3156,3157,3158,3159,3160,3161,3162,3163,3164,3165,3166,3167,3168,3169,3170,3171,3172,3173,3174,3175,3176,3177,3178,3179,3180,3181,3182,3183,3184,3185,3186,3187,3188,3189,3190,3191,3192,3193,3194,3195,3196,3197,3198,3199,3200,3201,3202,3203,3204,3205,3206,3207,3208,3209,3210,3211,3212,3213,3214,3215,3216,3217,3218,3219,3220,3221,3222,3223,3224,3225,3226,3227,3228,3229,3230,3231,3232,3233,3234,3235,3236,3237,3238,3239,3240,3241,3242,3243,3244,3245,3246,3247,3248,3249,3250,3251,3252,3253,3254,3255,3256,3257,3258,3259,3260,3261,3262,3263,3264,3265,3266,3267,3268,3269,3270,3271,3272,3273,3274,3275,3276,3277,3278,3279,3280,3281,3282,3283,3284,3285,3286,3287,3288,3289,3290,3291,3292,3293,3294,3295,3296,3297,3298,3299,3300,3301,3302,3303,3304,3305,3306,3307,3308,3309,3310,3311,3312,3313,3314,3315,3316,3317,3318,3319,3320,3321,3322,3323,3324,3325,3326,3327,3328,3329,3330,3331,3332,3333,3334,3335,3336,3337,3338,3339,3340,3341,3342,3343,3344,3345,3346,3347,3348,3349,3350,3351,3352,3353,3354,3355,3356,3357,3358,3359,3360,3361,3362,3363,3364,3365,3366,3367,3368,3369,3370,3371,3372,3373,3374,3375,3376,3377,3378,3379,3380,3381,3382,3383,3384,3385,3386,3387,3388]} -------------------------------------------------------------------------------- /test/fixtures/layer.json: -------------------------------------------------------------------------------- 1 | {"currentVersion":10.21,"id":0,"name":"Schools Map","type":"Feature Layer","displayField":"id","description":"","copyrightText":"","defaultVisibility":true,"relationships":[],"isDataVersioned":false,"supportsRollbackOnFailureParameter":true,"supportsStatistics":true,"supportsAdvancedQueries":true,"advancedQueryCapabilities":{"supportsPagination":true,"supportsQueryWithDistance":true,"supportsReturningQueryExtent":true,"supportsStatistics":true,"supportsOrderBy":true,"supportsDistinct":true},"geometryType":"esriGeometryPoint","minScale":0,"maxScale":0,"extent":{"xmin":-122.407827,"ymin":47.509701,"xmax":-122.258635,"ymax":47.725699,"spatialReference":{"wkid":4326,"latestWkid":4326}},"drawingInfo":{"renderer":{"type":"simple","symbol":{"color":[45,172,128,161],"outline":{"color":[190,190,190,105],"width":0.5,"type":"esriSLS","style":"esriSLSSolid"},"size":7.5,"type":"esriSMS","style":"esriSMSCircle"}},"labelingInfo":null},"hasM":false,"hasZ":false,"allowGeometryUpdates":true,"hasAttachments":false,"htmlPopupType":"esriServerHTMLPopupTypeNone","objectIdField":"id","globalIdField":"","typeIdField":"","fields":[{"name":"address","type":"esriFieldTypeString","alias":"address","length":128},{"name":"city_feature","type":"esriFieldTypeString","alias":"city_feature","length":128},{"name":"common_name","type":"esriFieldTypeDate","alias":"common_name"},{"name":"longitude","type":"esriFieldTypeString","alias":"longitude","length":128},{"name":"latitude","type":"esriFieldTypeString","alias":"latitude","length":128},{"name":"website_url","type":"esriFieldTypeString","alias":"website_url","length":128},{"name":"id","type":"esriFieldTypeOID","alias":"id"}],"types":[],"templates":[],"maxRecordCount":1000,"supportedQueryFormats":"JSON","hasStaticData":true,"capabilities":"Query","initialExtent":{"xmin":-122.407827,"ymin":47.509701,"xmax":-122.258635,"ymax":47.725699,"spatialReference":{"wkid":4326,"latestWkid":4326}},"fullExtent":{"xmin":-122.407827,"ymin":47.509701,"xmax":-122.258635,"ymax":47.725699,"spatialReference":{"wkid":4326,"latestWkid":4326}}} -------------------------------------------------------------------------------- /test/fixtures/layer10.0.json: -------------------------------------------------------------------------------- 1 | {"id":2,"name":"Fire Perimeter","type":"Feature Layer","displayField":"description","description":"","copyrightText":"","relationships":[],"geometryType":"esriGeometryPolygon","minScale":0,"maxScale":0,"extent":{"xmin":-379.3359375,"ymin":-134.273368448,"xmax":6696066.20249256,"ymax":8440532.08945883,"spatialReference":{"wkid":4326}},"drawingInfo":{"renderer":{"type":"simple","symbol":{"type":"esriSFS","style":"esriSFSSolid","color":[255,167,127,255],"outline":{"type":"esriSLS","style":"esriSLSSolid","color":[230,0,0,255],"width":2}},"label":"","description":""},"transparency":0,"labelingInfo" : null},"hasAttachments":false,"htmlPopupType":"esriServerHTMLPopupTypeAsHTMLText","objectIdField":"objectid","globalIdField":"","typeIdField":"","fields":[{"name":"objectid","type":"esriFieldTypeOID","alias":"Object ID","editable":false,"domain" : null},{"name":"name","type":"esriFieldTypeString","alias":"Name","editable":true,"length":50,"domain" : null},{"name":"description","type":"esriFieldTypeString","alias":"Description","editable":true,"length":1073741822,"domain" : null}],"types":[],"templates":[{"name":"Fire Perimeter","description":"","drawingTool":"esriFeatureEditToolPolygon","prototype":{"attributes":{"description" : null,"name" : null}}}],"capabilities":"Query,Editing"} -------------------------------------------------------------------------------- /test/fixtures/layerInfo.json: -------------------------------------------------------------------------------- 1 | {"currentVersion":10.2,"id":5,"name":"MPD - Theft from Auto - Last 30 days","type":"Feature Layer","description":"","geometryType":"esriGeometryPoint","copyrightText":"","parentLayer":null,"subLayers":[],"minScale":60000,"maxScale":0,"drawingInfo":{"renderer":{"type":"simple","symbol":{"type":"esriPMS","url":"c887b2415f3d40d2c739b6e6d8a06019","imageData":"iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IB2cksfwAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAr5JREFUOI29lE1IVFEUx393fOOb9zI1LUoDw74sB4sgKKicEtr0QUoboxa2MGhhQUlh44ypCyFcFK6qhdEkBVENJGW6kTa1LZOyhJpWNRVm2Rtf6JwW4wxvpikKogMP7r3vf37n/847XI1/HNp/AVpW+3xyZhsQVQuyHigEJoERhdyZnZm5nJfXZf0R0LID9chsD8JCEOerIsAnKJ9Lyz0dmw40GZ7OW78FWnawFaEzuR8dfUf0/Td21KzISJMSQd20pgOnTE9nd1agZbcdQCQFi8eFBwMvafUPMzB4kOrqTCgK1LnYdOC106kGEIsF8kWkx6keG4vS6h+mocHLxo1ljD57hzg6oFzg9S5RgqtnaqrlfrKn2lytw0CxE9h/dwyAI0c3Y5puIpHP7Nq9JtMlICU5mtYIXEgBBVXrlMzMxAldfQTA5UuPKS0pYM/eyiywOSSqLg0IrHMK3kYmGB+3AaipWU04PMKWreV0dz9MaXLdLo4d35rcViUXSWCBE/ji+YfUOvJmgu2+VWzeVEbdvj7sRB0qvEVOYCo/CZwkMWcArPUuYufOZQwNRWj1DwPQfPI+Z/zbKSzUEw5z0ybucyZwBPAlD8vLiyktzeN2uJ6FxQbV23qxbZiX5yY/35NIzHE5eOpJGlChwoL4HAricXBrLqrWlRAK1SFAWVkBHiPhwaUcQCXhdIfyvRflDgILAKLRKUKhUU40V6PrGstXFhHw36P/XiNZ4iOzOVfSgIbRNWnZwSaEawCDg68Itm2jomIRAB5dY+nSomwwUNJkmm1fM3uIqXf0xezgShHOHjq0IS1nbeViLl7anwVGwNQ7bziP0n6VoXe0W3ZwHKEn+fmOZGd8QqkmU2+/nlnjp+vL1Dv6YrGWflTuYUFqSQxtAYnReqqQMEKv4en48rPlX1ywhtE1CZyfe/4qfgAVgOHBVsJoEQAAAABJRU5ErkJggg==","contentType":"image/png","width":15,"height":15,"angle":0,"xoffset":0,"yoffset":0},"label":"","description":""},"transparency":0,"labelingInfo":null},"defaultVisibility":false,"extent":{"xmin":390519,"ymin":128114,"xmax":407313,"ymax":146728,"spatialReference":{"wkid":26985,"latestWkid":26985}},"hasAttachments":false,"htmlPopupType":"esriServerHTMLPopupTypeAsHTMLText","displayField":"CCN","typeIdField":null,"fields":[{"name":"CCN","type":"esriFieldTypeString","alias":"CCN","length":8,"domain":null},{"name":"REPORT_DAT","type":"esriFieldTypeDate","alias":"REPORT_DAT","length":36,"domain":null},{"name":"SHIFT","type":"esriFieldTypeString","alias":"SHIFT","length":50,"domain":null},{"name":"METHOD","type":"esriFieldTypeString","alias":"METHOD","length":50,"domain":null},{"name":"OFFENSE","type":"esriFieldTypeString","alias":"OFFENSE","length":250,"domain":null},{"name":"BLOCK","type":"esriFieldTypeString","alias":"BLOCK","length":100,"domain":null},{"name":"XBLOCK","type":"esriFieldTypeDouble","alias":"XBLOCK","domain":null},{"name":"YBLOCK","type":"esriFieldTypeDouble","alias":"YBLOCK","domain":null},{"name":"WARD","type":"esriFieldTypeString","alias":"WARD","length":1,"domain":null},{"name":"ANC","type":"esriFieldTypeString","alias":"ANC","length":5,"domain":null},{"name":"DISTRICT","type":"esriFieldTypeString","alias":"DISTRICT","length":1,"domain":null},{"name":"PSA","type":"esriFieldTypeString","alias":"PSA","length":3,"domain":null},{"name":"NEIGHBORHOOD_CLUSTER","type":"esriFieldTypeString","alias":"NEIGHBORHOOD_CLUSTER","length":200,"domain":null},{"name":"BLOCK_GROUP","type":"esriFieldTypeString","alias":"BLOCK_GROUP","length":20,"domain":null},{"name":"CENSUS_TRACT","type":"esriFieldTypeString","alias":"CENSUS_TRACT","length":20,"domain":null},{"name":"VOTING_PRECINCT","type":"esriFieldTypeString","alias":"VOTING_PRECINCT","length":25,"domain":null},{"name":"LATITUDE","type":"esriFieldTypeDouble","alias":"LATITUDE","domain":null},{"name":"LONGITUDE","type":"esriFieldTypeDouble","alias":"LONGITUDE","domain":null},{"name":"BID","type":"esriFieldTypeString","alias":"BID","length":100,"domain":null},{"name":"ESRI_OID","type":"esriFieldTypeOID","alias":"ESRI_OID","domain":null},{"name":"Shape","type":"esriFieldTypeGeometry","alias":"Shape","domain":null}],"relationships":[],"canModifyLayer":false,"canScaleSymbols":false,"hasLabels":false,"capabilities":"Map,Query,Data","maxRecordCount":1000,"supportsStatistics":true,"supportsAdvancedQueries":true,"supportedQueryFormats":"JSON, AMF","ownershipBasedAccessControlForFeatures":{"allowOthersToQuery":true},"useStandardizedQueries":true} -------------------------------------------------------------------------------- /test/fixtures/layerNoPaging.json: -------------------------------------------------------------------------------- 1 | {"currentVersion":10.2,"id":50,"name":"Metro Station Entrances","type":"Feature Layer","description":"","geometryType":"esriGeometryPoint","copyrightText":"","parentLayer":null,"subLayers":[],"minScale":135000,"maxScale":0,"drawingInfo":{"renderer":{"type":"simple","symbol":{"type":"esriPMS","url":"f83da164b54467fc9337c274affeac32","imageData":"iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAAAXNSR0IB2cksfwAAAAlwSFlzAAAOxAAADsQBlSsOGwAAASNJREFUGJV1zr9LAnEYx/G38j1Nw+p0KmjooMITpOCgwJIgqMmGtgocQmjpH+h/aGiOs8F/4Wi4wcktGiTQwcLsh0lB2g+9uoJsKKqL8zM9w+t5Po8A2AHJShHufuDBJdYwLV3HFgDvKX9RwlbxulHw3XKTyaAIAEXbUmMziwAUC4dcneyzsH5ASI7w9mphZtdGgg1kATCmaiSXVwDoCwRpVEyWVjeQfD6s9jNm9qtBOH7rtBlVJogmNn/g3zhw/aKKMhlD1eZ46bRpNe+RwxF3XDstM67GiWsJLqsVAv2h3pebd3WeHh8YGByiVikRnZ7tjQGuz89QpzRKR3kH9njpCoB8Ll3I59LzAMfG7+Letv49+ctBw24KgF2D5P8GZ2wAPgFqslkFrCAYOAAAAABJRU5ErkJggg==","contentType":"image/png","width":8,"height":8,"angle":0,"xoffset":0,"yoffset":0},"label":"","description":""},"transparency":0,"labelingInfo":null},"defaultVisibility":false,"extent":{"xmin":-8581149.950334396,"ymin":4699438.5454374775,"xmax":-8564327.000011444,"ymax":4718217.531164601,"spatialReference":{"wkid":102100,"latestWkid":3857}},"hasAttachments":false,"htmlPopupType":"esriServerHTMLPopupTypeNone","displayField":"NAME","typeIdField":null,"fields":[{"name":"OBJECTID_1","type":"esriFieldTypeOID","alias":"OBJECTID_1","domain":null},{"name":"GIS_ID","type":"esriFieldTypeString","alias":"GIS_ID","length":16,"domain":null},{"name":"NAME","type":"esriFieldTypeString","alias":"NAME","length":48,"domain":null},{"name":"WEB_URL","type":"esriFieldTypeString","alias":"WEB_URL","length":100,"domain":null},{"name":"EXIT_TO_ST","type":"esriFieldTypeString","alias":"EXIT.TO.STreet","length":254,"domain":null},{"name":"FEATURECOD","type":"esriFieldTypeSmallInteger","alias":"FEATURECOD","domain":null},{"name":"DESCRIPTIO","type":"esriFieldTypeString","alias":"DESCRIPTIO","length":60,"domain":null},{"name":"CAPTUREYEA","type":"esriFieldTypeDate","alias":"CAPTUREYEA","length":8,"domain":null},{"name":"LINE","type":"esriFieldTypeString","alias":"LINE","length":50,"domain":null},{"name":"ADDRESS_ID","type":"esriFieldTypeDouble","alias":"ADDRESS_ID","domain":null},{"name":"SHAPE","type":"esriFieldTypeGeometry","alias":"Shape","domain":null}],"relationships":[],"canModifyLayer":false,"canScaleSymbols":false,"hasLabels":false,"capabilities":"Map,Query,Data","maxRecordCount":1000,"supportsStatistics":true,"supportsAdvancedQueries":true,"supportedQueryFormats":"JSON, AMF","ownershipBasedAccessControlForFeatures":{"allowOthersToQuery":true},"useStandardizedQueries":true} -------------------------------------------------------------------------------- /test/fixtures/layerPaging.json: -------------------------------------------------------------------------------- 1 | {"currentVersion":10.3,"id":0,"name":"Rail Crossings (INDOT)","type":"Feature Layer","description":"Railroad Crossings (INDOT), 2006 (1:1,200) - Shows locations of rail crossings in Indiana. Attributes include the identification number ('XING_ID') for the Federal Railroad Administration (USDOT) National Highway-Rail Crossing Inventory Program. For more information, see the following URL: http://www.fra.dot.gov/us/content/801\nData were derived from a pre-existing INDOT Road basemap, the 2003 imagery of the National Agricultural Imagery Program (NAIP), and Railroad Database and Programatic Selection Processes. Provided by the Indiana Department of Transportation (INDOT), Business Information and Technology Systems, GIS Mapping. Data are current as of September 13, 2006.","geometryType":"esriGeometryPoint","copyrightText":"copyright 2011","parentLayer":null,"subLayers":[],"minScale":0,"maxScale":0,"drawingInfo":{"renderer":{"type":"simple","symbol":{"type":"esriPMS","url":"d6576a6694cd2441ba038f21d73414cb","imageData":"iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAAAXNSR0IB2cksfwAAAAlwSFlzAAAOxAAADsQBlSsOGwAAAe9JREFUKJGlkz1oU2EUhp/z5d7ENDH9SRHSwcFFRETaWBBd3FwSsYO4dbCQLCLS2KE4tHWxtERxswFdXASnkA66CSo4eOmgFMRFkNYWE1uIbdPc23sc2oT+XHDwjAce3vec9xyL/ygrqOm6bl8oFEqLSApwRMT5J7wHTQjk3NVV/EYDMQa/2URsOy8ipUBYVdPApFetZtzvi0ROxrG7FSWM+2MNv2HPqWpWRLJBypM79Xpm/f5dsJpExkcgFEaA5ucF6tcLdC8uZlQ113Jg7VPNeMvf6CrcoDY6w++Hz+gZH2Hz0xfqQ/dIlIuYSAN13TmgtF85DSBuFSuVIPlojNroLNXb0+yU35IoF4ld7kdDiruygqqmRcRpK4sI+FuAjZXsJHrzKhvDE4SGrhAdOAuA+Fug2hJzDmxbsQHY+LDAxvAE8RcP2Hz5uj0CVhjT0QHgtG3vLSCHneTPu/e7M84/JnbxPNHBc9QKs6xNPyd2K8+xgV5aubdgR1Wd8OnB9NabeRKVXRDA6u0kWRxj/ckr7FP9qGo767Ztz/OuWZa11HVniu2vH2ku/QK2QSIQPkHPzFNM/LgjIvkjsG3by6p6AWMqkTOXUhwqVS15njcVeGEt+0Cf7/tZEUnt5f9TRCrGmCP3HfgYxphKUP9w/QUXmsJhdVmCRgAAAABJRU5ErkJggg==","contentType":"image/png","width":11,"height":11,"angle":0,"xoffset":0,"yoffset":0},"label":"","description":""},"transparency":0,"labelingInfo":null},"defaultVisibility":true,"extent":{"xmin":411733.0012562731,"ymin":4189067.787922809,"xmax":688101.2267936617,"ymax":4625312.943145094,"spatialReference":{"wkid":26916,"latestWkid":26916}},"hasAttachments":false,"htmlPopupType":"esriServerHTMLPopupTypeNone","displayField":"XING_ID","typeIdField":null,"fields":[{"name":"OBJECTID","type":"esriFieldTypeOID","alias":"OBJECTID","domain":null},{"name":"XING_ID","type":"esriFieldTypeString","alias":"Federal Crossing ID (USDOT)","length":50,"domain":null},{"name":"Shape","type":"esriFieldTypeGeometry","alias":"Shape","domain":null},{"name":"GlobalID","type":"esriFieldTypeGlobalID","alias":"GlobalID","length":38,"domain":null}],"relationships":[],"canModifyLayer":false,"canScaleSymbols":false,"hasLabels":false,"capabilities":"Map,Query,Data","maxRecordCount":100,"supportsStatistics":true,"supportsAdvancedQueries":true,"supportedQueryFormats":"JSON, AMF","ownershipBasedAccessControlForFeatures":{"allowOthersToQuery":true},"useStandardizedQueries":true,"advancedQueryCapabilities":{"useStandardizedQueries":true,"supportsStatistics":true,"supportsOrderBy":true,"supportsDistinct":true,"supportsPagination":true,"supportsTrueCurve":true}} -------------------------------------------------------------------------------- /test/fixtures/layerStatsFail.json: -------------------------------------------------------------------------------- 1 | {"currentVersion":10.2,"id":8,"name":"Crime Incidents - Last 30 Days","type":"Feature Layer","description":"","geometryType":"esriGeometryPoint","copyrightText":"","parentLayer":null,"subLayers":[],"minScale":10000,"maxScale":0,"drawingInfo":{"renderer":{"type":"simple","symbol":{"type":"esriPMS","url":"1a0fa88824a7e95b7b7d6a13eb227c8c","imageData":"iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAAXNSR0IB2cksfwAAAAlwSFlzAAAOxAAADsQBlSsOGwAABOFJREFUSInNlm1sk1UUx3/3acfarWMwN3Qb2+RlA7cxcEMUggplOEGRN4UxRNQYURIEkQ+A6ZxbBBME/eB7wJdIRqPRxER8CcrGgjCCI9tgmMEQNsg27NjopO3TtX2uH9aOtisvhoT4T56kz33Ovb97zuk95+q5zdL/L4EuV3m6FJ4lAjFTQh6QCCiADUQT8KuQ2h6jseLCLQH7Qd4tEm8xCJ0cbJIKMhV4RAqx1am+USmktvl64GsCnW5LsZTeT4ChN9q1XzqQK6QQC5xuy4sx0RXWmwa61NJXpWQ7IADa7XYu2C+TPzINvaKE2DZfvEifz8uElNTAUBxS7HGppSlGQ/mOGwL7PbsKAzhts1F2opKxJzNYmTWdyWnptHTZsDb/we+X63kqeUYwEAAJ251uS3u4pyFAl6s83R9GETx+8lI7AC1qK5bGVnTHwReU0AO2Rl6WDyJEyDSQ4lOXy3IwOKf60O/erYTlTEpJja0ZgHzTOGal5pFgNODs81DbeZp93XX87e2my+EgyWQKD1icFMoW4JlBQFUty9DQioOtPT4fh8+dpdPbwevZxZjHZqIoAk3TUBQFc1YW89onUVq3m19O/cn8nAnERRvCmLLE5bIM/HMHgH6YAqB6vBxpPcdnp6po6+tgXeZCCrOyON/Tza7Gw8RHReP0ukk0xLMkN583859m7dFdWM/v57nRj2Iek8nwmJjA0joplGXAtrCQypmB1FU2HuWr8/sAiBXRFGWNB2Bnw2Hmjsrm/oxRqB4Pa/d/w2JtEnmpqTzcUsCBnjreP/M9By+OY5v5SfSKLrB4YQSgyA38KsmbTFNPO8euNDE9YRKGqKgBqx3H97Lo8lTGDE/ilbwZDDX0h3DKiNEc6KljmC6BjVPmBsMAmTMoh/SXKwAMUVFsfKCI1VUdxA8xDhisLniI6r/uorqzmY/P7iVRl8BH5udJ1OsxDenfVMXkRdw5NC4sjyRFAoZamEysy53DD+eaBsY21Fj5YNYKlk4soNP+Dy9Uf0hbdxeJsenYHA5WphWRm5wSaTktErALCDm9drWPWns9titmkkwmFAHvHfmN2RnZOPo8RCmQnpCIx+fj5/Z6suJCD3/Y2uFAeQJEyIzGrjYAvmioZf1UM9sfXo5Rr+fMJRsGvZ7PC9cwzBjDd8cbaFFb6XB3sl4zowsrfwIaI3lYDRQFXryaj5ruegB+tB1CO+Tj2bxpxMbGkOcvY72qyu5jR9nV9hMADumm3d5L2vBhIUCJrBoEVNDt0dDewn8WHe4+Vt5dGDJxdc2XpEUnMTLmDi71Oam117MibTYvjXpswMbr84aH0ydk1NeDgAZDWatTLbUCJQDxRiNLJxaEzMwZkcyaIztpcPS/r8tcyPycCeGAcFmNxtK2QUAAIfWbpPA+zjV6YG5yChvGLead5m95YsQ05t2TG8ksWL1C6jcHD4QAjcbSNqfbsgopKgnrGAHNGZ+NXXWyIGciihLRJCCJkKuMhqveDQICxERXWF1qabIktCcGpAhByb33XQ8EIAW8ZozQ9SMefKOh/F2n29KBFP/lihFQL0KuigS7JhD8nrrKD/l75EAnuY40wCqkflN4GG8KCP05BZaratlmDd8yYIa/yAfqbpe/YFQpKFaDoaz1Bpu6uXupf6G3/c8t6bbfvP8FLAvQPqFEIfAAAAAASUVORK5CYII=","contentType":"image/png","width":21,"height":21,"angle":0,"xoffset":0,"yoffset":0},"label":"","description":""},"transparency":0,"labelingInfo":null},"defaultVisibility":false,"extent":{"xmin":390519,"ymin":127432.2,"xmax":407795,"ymax":146841,"spatialReference":{"wkid":26985,"latestWkid":26985}},"hasAttachments":false,"htmlPopupType":"esriServerHTMLPopupTypeAsHTMLText","displayField":"CCN","typeIdField":null,"fields":[{"name":"CCN","type":"esriFieldTypeString","alias":"CCN","length":16,"domain":null},{"name":"REPORTDATETIME","type":"esriFieldTypeDate","alias":"REPORTDATETIME","length":36,"domain":null},{"name":"SHIFT","type":"esriFieldTypeString","alias":"SHIFT","length":50,"domain":null},{"name":"OFFENSE","type":"esriFieldTypeString","alias":"OFFENSE","length":250,"domain":null},{"name":"METHOD","type":"esriFieldTypeString","alias":"METHOD","length":50,"domain":null},{"name":"LASTMODIFIEDDATE","type":"esriFieldTypeDate","alias":"LASTMODIFIEDDATE","length":36,"domain":null},{"name":"BLOCKSITEADDRESS","type":"esriFieldTypeString","alias":"BLOCKSITEADDRESS","length":100,"domain":null},{"name":"BLOCKXCOORD","type":"esriFieldTypeDouble","alias":"BLOCKXCOORD","domain":null},{"name":"BLOCKYCOORD","type":"esriFieldTypeDouble","alias":"BLOCKYCOORD","domain":null},{"name":"WARD","type":"esriFieldTypeString","alias":"WARD","length":1,"domain":null},{"name":"ANC","type":"esriFieldTypeString","alias":"ANC","length":5,"domain":null},{"name":"DISTRICT","type":"esriFieldTypeString","alias":"DISTRICT","length":7,"domain":null},{"name":"PSA","type":"esriFieldTypeString","alias":"PSA","length":3,"domain":null},{"name":"NEIGHBORHOODCLUSTER","type":"esriFieldTypeString","alias":"NEIGHBORHOODCLUSTER","length":193,"domain":null},{"name":"BUSINESSIMPROVEMENTDISTRICT","type":"esriFieldTypeString","alias":"BUSINESSIMPROVEMENTDISTRICT","length":100,"domain":null},{"name":"BLOCK_GROUP","type":"esriFieldTypeString","alias":"BLOCK_GROUP","length":20,"domain":null},{"name":"CENSUS_TRACT","type":"esriFieldTypeString","alias":"CENSUS_TRACT","length":20,"domain":null},{"name":"VOTING_PRECINCT","type":"esriFieldTypeString","alias":"VOTING_PRECINCT","length":25,"domain":null},{"name":"START_DATE","type":"esriFieldTypeDate","alias":"START_DATE","length":36,"domain":null},{"name":"END_DATE","type":"esriFieldTypeDate","alias":"END_DATE","length":36,"domain":null},{"name":"ESRI_OID","type":"esriFieldTypeOID","alias":"ESRI_OID","domain":null},{"name":"Shape","type":"esriFieldTypeGeometry","alias":"Shape","domain":null}],"relationships":[],"canModifyLayer":false,"canScaleSymbols":false,"hasLabels":false,"capabilities":"Map,Query,Data","maxRecordCount":1000,"supportsStatistics":true,"supportsAdvancedQueries":true,"supportedQueryFormats":"JSON, AMF","ownershipBasedAccessControlForFeatures":{"allowOthersToQuery":true},"useStandardizedQueries":true} -------------------------------------------------------------------------------- /test/fixtures/objectIds.json: -------------------------------------------------------------------------------- 1 | {"objectIdField":"id","objectIds":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82]} -------------------------------------------------------------------------------- /test/fixtures/secured.json: -------------------------------------------------------------------------------- 1 | {"error":{"code":499,"message":"Token Required","details":[]}} -------------------------------------------------------------------------------- /test/fixtures/serviceInfo.json: -------------------------------------------------------------------------------- 1 | {"currentVersion":10.3,"serviceDescription":"Victoria's pre-approved roads for Oversize Overmass (OSOM) Load Carrying Vehicle Scheme Permit Network.","hasVersionedData":false,"supportsDisconnectedEditing":false,"hasStaticData":false,"maxRecordCount":30000,"supportedQueryFormats":"JSON","capabilities":"Query","description":"\u003cp\u003eVictoria's gazetted roads for Over Mass Over Size. This Class 2 network shows roads mapped in three categories: Approved, Approved - Conditions Apply and Restricted\u003c/p\u003e\u003cp\u003e\u003ca href='http://data.vicroads.vic.gov.au/metadata/OSOM_SchemePermitNetwork%20-%20Open%20Data.html' target='_blank'\u003eAbout this dataset\u003c/a\u003e - \u003ca href='mailto:sissupport@roads.vic.gov.au' style='line-height: 1.38461538461538;' target='_blank'\u003eContact us\u003c/a\u003e\u003c/p\u003e","copyrightText":"VicRoads, National Heavy Vehicle Regulator","spatialReference":{"wkid":102100,"latestWkid":3857},"initialExtent":{"xmin":15456869.337833311,"ymin":-4508807.3045889772,"xmax":16758621.941338524,"ymax":-3901587.3401490455,"spatialReference":{"wkid":102100,"latestWkid":3857}},"fullExtent":{"xmin":15691939.723399997,"ymin":-4680313.4448000006,"xmax":16662035.512200002,"ymax":-4044192.2406,"spatialReference":{"wkid":102100,"latestWkid":3857}},"allowGeometryUpdates":true,"units":"esriMeters","size":8650752,"syncEnabled":false,"editorTrackingInfo":{"enableEditorTracking":false,"enableOwnershipAccessControl":false,"allowOthersToUpdate":true,"allowOthersToDelete":false},"xssPreventionInfo":{"xssPreventionEnabled":true,"xssPreventionRule":"InputOnly","xssInputRule":"rejectInvalid"},"layers":[{"id":0,"name":"HVR_OSOM_Scheme_Permit_WM","parentLayerId":-1,"defaultVisibility":true,"subLayerIds":null,"minScale":0,"maxScale":0}],"tables":[]} -------------------------------------------------------------------------------- /test/fixtures/stats.json: -------------------------------------------------------------------------------- 1 | {"objectIdFieldName":"id","globalIdFieldName":"","hasZ":false,"hasM":false,"geometryType":"esriGeometryPoint","spatialReference":{"wkid":4326},"fields":[{"name":"min_id","type":"esriFieldTypeInteger","alias":"min_id"},{"name":"max_id","type":"esriFieldTypeInteger","alias":"max_id"}],"features":[{"attributes":{"min_id":1,"max_id":14}}]} 2 | -------------------------------------------------------------------------------- /test/fixtures/statsCaps.json: -------------------------------------------------------------------------------- 1 | {"objectIdFieldName":"id","globalIdFieldName":"","hasZ":false,"hasM":false,"geometryType":"esriGeometryPoint","spatialReference":{"wkid":4326},"fields":[{"name":"min_id","type":"esriFieldTypeInteger","alias":"min_id"},{"name":"max_id","type":"esriFieldTypeInteger","alias":"max_id"}],"features":[{"attributes":{"MIN_ID":1,"MAX_ID":14}}]} 2 | -------------------------------------------------------------------------------- /test/fixtures/statsFail.json: -------------------------------------------------------------------------------- 1 | {"error":{"code":400,"message":"Invalid or missing input parameters.","details":[]}} -------------------------------------------------------------------------------- /test/fixtures/statsFailedPaging.json: -------------------------------------------------------------------------------- 1 | {"displayFieldName":"","fieldAliases":{"min_OBJECTID_1":"min_OBJECTID_1","max_OBJECTID_1":"max_OBJECTID_1"},"fields":[{"name":"min_OBJECTID_1","type":"esriFieldTypeDouble","alias":"min_OBJECTID_1"},{"name":"max_OBJECTID_1","type":"esriFieldTypeDouble","alias":"max_OBJECTID_1"}],"features":[{"attributes":{"min_OBJECTID_1":1,"max_OBJECTID_1":15592}}]} -------------------------------------------------------------------------------- /test/fixtures/statsNoPaging.json: -------------------------------------------------------------------------------- 1 | {"displayFieldName":"","fieldAliases":{"min_OBJECTID_1":"min_OBJECTID_1","max_OBJECTID_1":"max_OBJECTID_1"},"fields":[{"name":"min_OBJECTID_1","type":"esriFieldTypeDouble","alias":"min_OBJECTID_1"},{"name":"max_OBJECTID_1","type":"esriFieldTypeDouble","alias":"max_OBJECTID_1"}],"features":[{"attributes":{"min_OBJECTID_1":1,"max_OBJECTID_1":88}}]} -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var sinon = require('sinon') 2 | var test = require('tape') 3 | var FeatureService = require('../') 4 | var Utils = require('../lib/utils') 5 | var nock = require('nock') 6 | var fs = require('fs') 7 | var _ = require('lodash') 8 | var zlib = require('zlib') 9 | 10 | var service = new FeatureService('http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1', {objectIdField: 'OBJECTID'}) 11 | 12 | var serviceFixture = require('./fixtures/serviceInfo.json') 13 | var layerInfo = require('./fixtures/layerInfo.json') 14 | var hostedLayerInfo = require('./fixtures/hostedLayerInfo.json') 15 | var layerFixture = require('./fixtures/layer.json') 16 | var idFixture = require('./fixtures/objectIds.json') 17 | var countFixture = require('./fixtures/count.json') 18 | var securedFixture = require('./fixtures/secured.json') 19 | var statsFixture = require('./fixtures/stats.json') 20 | var statsFixtureCaps = require('./fixtures/statsCaps.json') 21 | 22 | test('create a service with query strings in the parameters', function (t) { 23 | var serv = new FeatureService('http://koop.whatever.com/FeatureServer/2?f=json', {layer: '2?f=json'}) 24 | t.equal(serv.layer.toString(), '2') 25 | t.equal(serv.server, 'http://koop.whatever.com/FeatureServer') 26 | t.end() 27 | }) 28 | 29 | test('create a service when the url has a trailing slash', function (t) { 30 | var serv = new FeatureService('http://koop.whatava.com/FeatureServer/0/', {layer: 0}) 31 | t.equal(serv.layer.toString(), '0') 32 | t.equal(serv.server, 'http://koop.whatava.com/FeatureServer') 33 | t.end() 34 | }) 35 | 36 | test('override layer with passed in option', function (t) { 37 | var serv = new FeatureService('http://koop.whateva.com/FeatureServer/1', {layer: '3'}) 38 | t.equal(serv.layer.toString(), '3') 39 | t.end() 40 | }) 41 | 42 | test('override layer with passed in option that is not a string', function (t) { 43 | var serv = new FeatureService('http://koop.whateva.com/FeatureServer/1', {layer: 3}) 44 | t.equal(serv.layer.toString(), '3') 45 | t.end() 46 | }) 47 | 48 | test('get the objectId', function (t) { 49 | var oid = service.getObjectIdField(layerInfo) 50 | t.equal(oid, 'ESRI_OID') 51 | 52 | t.end() 53 | }) 54 | 55 | test('get the from a hosted service', function (t) { 56 | var oid = service.getObjectIdField(hostedLayerInfo) 57 | t.equal(oid, 'FID') 58 | 59 | t.end() 60 | }) 61 | 62 | test('try to get the objectId when there are no fields', function (t) { 63 | var layer = _.cloneDeep(layerInfo) 64 | delete layer.fields 65 | var oid = service.getObjectIdField(layer) 66 | t.equal(oid, false) 67 | 68 | t.end() 69 | }) 70 | 71 | test('build range pages', function (t) { 72 | var pages 73 | var stats = {min: 0, max: 2000} 74 | pages = service._rangePages(stats, stats.max / 2) 75 | t.equal(pages[0].req, 'http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1/query?outSR=4326&where=OBJECTID>=0+AND+OBJECTID<=999&f=json&outFields=*&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=') 76 | t.equal(pages[1].req, 'http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1/query?outSR=4326&where=OBJECTID>=1000+AND+OBJECTID<=2000&f=json&outFields=*&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=') 77 | t.equal(pages.length, 2) 78 | pages = service._rangePages(stats, stats.max / 4) 79 | t.equal(pages.length, 4) 80 | t.end() 81 | }) 82 | 83 | test('build range pages with output spatial reference 4629', function (t) { 84 | var pages 85 | var stats = {min: 0, max: 2000} 86 | var service = new FeatureService('http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1', {objectIdField: 'OBJECTID', outSR: 4629}) 87 | pages = service._rangePages(stats, stats.max / 2, true) 88 | t.equal(pages[0].req, 'http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1/query?outSR=4629&where=OBJECTID>=0+AND+OBJECTID<=999&f=json&outFields=*&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=') 89 | t.equal(pages[1].req, 'http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1/query?outSR=4629&where=OBJECTID>=1000+AND+OBJECTID<=2000&f=json&outFields=*&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=') 90 | t.equal(pages.length, 2) 91 | t.end() 92 | }) 93 | 94 | test('build id based pages', function (t) { 95 | var ids = [0, 1, 2, 3, 4, 5] 96 | var maxCount = 2 97 | var pages = service._idPages(ids, maxCount) 98 | t.equal(pages.length, 3) 99 | t.equal(pages[0].req, 'http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1/query?outSR=4326&where=OBJECTID >= 0 AND OBJECTID<=1&f=json&outFields=*&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=10') 100 | t.equal(pages[1].req, 'http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1/query?outSR=4326&where=OBJECTID >= 2 AND OBJECTID<=3&f=json&outFields=*&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=10') 101 | t.equal(pages[2].req, 'http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1/query?outSR=4326&where=OBJECTID >= 4 AND OBJECTID<=5&f=json&outFields=*&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=10') 102 | t.end() 103 | }) 104 | 105 | test('build id based pages with output spatial reference 4629', function (t) { 106 | var ids = [0, 1, 2, 3, 4, 5] 107 | var maxCount = 2 108 | var service = new FeatureService('http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1', {objectIdField: 'OBJECTID', outSR: 4629}) 109 | var pages = service._idPages(ids, maxCount, true) 110 | t.equal(pages.length, 3) 111 | t.equal(pages[0].req, 'http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1/query?outSR=4629&where=OBJECTID >= 0 AND OBJECTID<=1&f=json&outFields=*&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=10') 112 | t.equal(pages[1].req, 'http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1/query?outSR=4629&where=OBJECTID >= 2 AND OBJECTID<=3&f=json&outFields=*&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=10') 113 | t.equal(pages[2].req, 'http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1/query?outSR=4629&where=OBJECTID >= 4 AND OBJECTID<=5&f=json&outFields=*&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=10') 114 | t.end() 115 | }) 116 | 117 | test('build result offset pages', function (t) { 118 | var maxCount = 100 119 | var pages = service._offsetPages(4, maxCount) 120 | t.equal(pages[0].req, 'http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1/query?outSR=4326&f=json&outFields=*&where=1=1&resultOffset=0&resultRecordCount=100&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=') 121 | t.equal(pages[1].req, 'http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1/query?outSR=4326&f=json&outFields=*&where=1=1&resultOffset=100&resultRecordCount=100&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=') 122 | t.equal(pages.length, 4) 123 | t.end() 124 | }) 125 | 126 | test('build result offset pages with output spatial reference 4629', function (t) { 127 | var maxCount = 100 128 | var service = new FeatureService('http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1', {objectIdField: 'OBJECTID', outSR: 4629}) 129 | var pages = service._offsetPages(4, maxCount, true) 130 | t.equal(pages[0].req, 'http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1/query?outSR=4629&f=json&outFields=*&where=1=1&resultOffset=0&resultRecordCount=100&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=') 131 | t.equal(pages[1].req, 'http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1/query?outSR=4629&f=json&outFields=*&where=1=1&resultOffset=100&resultRecordCount=100&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=') 132 | t.equal(pages.length, 4) 133 | t.end() 134 | }) 135 | 136 | test('creates an out statistics url', function (t) { 137 | var url = service._statsUrl('test', ['min', 'max']) 138 | t.equal(url, 'http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1/query?f=json&outFields=&outStatistics=[{"statisticType":"min","onStatisticField":"test","outStatisticFieldName":"min_test"},{"statisticType":"max","onStatisticField":"test","outStatisticFieldName":"max_test"}]') 139 | t.end() 140 | }) 141 | 142 | test('get the metadata for a layer on the service', function (t) { 143 | sinon.stub(service, 'request').callsFake(function (url, callback) { 144 | callback(null, layerFixture) 145 | }) 146 | service.layerInfo(function (err, metadata) { 147 | t.equal(err, null) 148 | t.equal(service.request.calledWith('http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1?f=json'), true) 149 | service.request.restore() 150 | t.end() 151 | }) 152 | }) 153 | 154 | test('get the metadata for a service', function (t) { 155 | sinon.stub(service, 'request').callsFake(function (url, callback) { 156 | callback(null, serviceFixture) 157 | }) 158 | service.info(function (err, metadata) { 159 | t.equal(err, null) 160 | t.equal(service.request.calledWith('http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer?f=json'), true) 161 | service.request.restore() 162 | t.end() 163 | }) 164 | }) 165 | 166 | test('get the info for a service once it has already been saved', function (t) { 167 | t.plan(3) 168 | var service = new FeatureService('http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1', {objectIdField: 'OBJECTID'}) 169 | service._info = 'foo' 170 | t.equal(service.info(), 'foo') 171 | service.info(function (err, info) { 172 | t.error(err) 173 | t.equal(info, 'foo') 174 | }) 175 | }) 176 | 177 | test('get the metadata for a service once it has already been saved', function (t) { 178 | t.plan(3) 179 | var service = new FeatureService('http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1', {objectIdField: 'OBJECTID'}) 180 | service._metadata = 'foo' 181 | t.equal(service.metadata(), 'foo') 182 | service.metadata(function (err, info) { 183 | t.error(err) 184 | t.equal(info, 'foo') 185 | }) 186 | }) 187 | 188 | test('get the metadata for a layer once it has already been saved', function (t) { 189 | t.plan(3) 190 | var service = new FeatureService('http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1', {objectIdField: 'OBJECTID'}) 191 | service._layerInfo = 'foo' 192 | t.equal(service.layerInfo(), 'foo') 193 | service.layerInfo(function (err, info) { 194 | t.error(err) 195 | t.equal(info, 'foo') 196 | }) 197 | }) 198 | 199 | test('get all the object ids for a layer on the service', function (t) { 200 | sinon.stub(service, 'request').callsFake(function (url, callback) { 201 | callback(null, idFixture) 202 | }) 203 | service.layerIds(function (err, metadata) { 204 | t.equal(err, null) 205 | var expected = 'http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1/query?where=1=1&returnIdsOnly=true&f=json' 206 | t.equal(service.request.calledWith(expected), true) 207 | service.request.restore() 208 | t.end() 209 | }) 210 | }) 211 | 212 | test('get the range of object ids for a service', function (t) { 213 | sinon.stub(service, 'request').callsFake(function (url, callback) { 214 | callback(null, statsFixture) 215 | }) 216 | service.getObjectIdRange('id', function (err, range) { 217 | t.equal(err, null) 218 | var expected = 'http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1/query?f=json&outFields=&outStatistics=[{"statisticType":"min","onStatisticField":"id","outStatisticFieldName":"min_id"},{"statisticType":"max","onStatisticField":"id","outStatisticFieldName":"max_id"}]' 219 | t.equal(service.request.calledWith(expected), true) 220 | t.equal(range.min, 1) 221 | t.equal(range.max, 14) 222 | service.request.restore() 223 | t.end() 224 | }) 225 | }) 226 | 227 | test('get the range of object ids for a service when the attribute names are unexpectedly capitalized', function (t) { 228 | sinon.stub(service, 'request').callsFake(function (url, callback) { 229 | callback(null, statsFixtureCaps) 230 | }) 231 | service.getObjectIdRange('id', function (err, range) { 232 | t.equal(err, null) 233 | var expected = 'http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1/query?f=json&outFields=&outStatistics=[{"statisticType":"min","onStatisticField":"id","outStatisticFieldName":"min_id"},{"statisticType":"max","onStatisticField":"id","outStatisticFieldName":"max_id"}]' 234 | t.equal(service.request.calledWith(expected), true) 235 | t.equal(range.min, 1) 236 | t.equal(range.max, 14) 237 | service.request.restore() 238 | t.end() 239 | }) 240 | }) 241 | 242 | test('get the feature count for a layer on the service', function (t) { 243 | sinon.stub(service, 'request').callsFake(function (url, callback) { 244 | callback(null, countFixture) 245 | }) 246 | service.featureCount(function (err, metadata) { 247 | t.equal(err, null) 248 | var expected = 'http://koop.dc.esri.com/socrata/seattle/2tje-83f6/FeatureServer/1/query?where=1=1&returnCountOnly=true&f=json' 249 | t.equal(service.request.calledWith(expected), true) 250 | service.request.restore() 251 | t.end() 252 | }) 253 | }) 254 | 255 | test('get a json error when trying to get a feature count', function (t) { 256 | sinon.stub(service, 'request').callsFake(function (url, callback) { 257 | callback(null, securedFixture) 258 | }) 259 | service.featureCount(function (err, count) { 260 | t.notEqual(typeof err, 'undefined') 261 | t.equal(err.code, 499) 262 | t.equal(err.body.message, 'Token Required') 263 | service.request.restore() 264 | t.end() 265 | }) 266 | }) 267 | 268 | test('get an error with no response body when trying to get a feature count', function (t) { 269 | sinon.stub(service, 'request').callsFake(function (url, callback) { 270 | callback(new Error(), null) 271 | }) 272 | service.featureCount(function (err, count) { 273 | t.equal(err.code, 500) 274 | service.request.restore() 275 | t.end() 276 | }) 277 | }) 278 | 279 | test('get a json error when trying to get layer ids', function (t) { 280 | sinon.stub(service, 'request').callsFake(function (url, callback) { 281 | callback(null, securedFixture) 282 | }) 283 | service.layerIds(function (err, count) { 284 | t.notEqual(typeof err, 'undefined') 285 | t.equal(err.code, 499) 286 | t.equal(err.body.message, 'Token Required') 287 | service.request.restore() 288 | t.end() 289 | }) 290 | }) 291 | 292 | test('get an error with no response body when trying to get layer ids', function (t) { 293 | sinon.stub(service, 'request').callsFake(function (url, callback) { 294 | callback(new Error(), null) 295 | }) 296 | service.layerIds(function (err, count) { 297 | t.equal(err.code, 500) 298 | service.request.restore() 299 | t.end() 300 | }) 301 | }) 302 | 303 | test('get a json error when trying to get layer info', function (t) { 304 | sinon.stub(service, 'request').callsFake(function (url, callback) { 305 | callback(null, securedFixture) 306 | }) 307 | service.layerInfo(function (err, count) { 308 | t.notEqual(typeof err, 'undefined') 309 | t.equal(err.code, 499) 310 | t.equal(err.body.message, 'Token Required') 311 | service.request.restore() 312 | t.end() 313 | }) 314 | }) 315 | 316 | test('get an error with no response body when trying to get layer info', function (t) { 317 | sinon.stub(service, 'request').callsFake(function (url, callback) { 318 | callback(new Error(), null) 319 | }) 320 | service.layerInfo(function (err, count) { 321 | t.equal(err.code, 500) 322 | service.request.restore() 323 | t.end() 324 | }) 325 | }) 326 | 327 | test('get a json error when trying to get statistics', function (t) { 328 | sinon.stub(service, 'request').callsFake(function (url, callback) { 329 | callback(null, securedFixture) 330 | }) 331 | service.statistics('foo', ['max'], function (err, count) { 332 | t.notEqual(typeof err, 'undefined') 333 | t.equal(err.code, 499) 334 | t.equal(err.body.message, 'Token Required') 335 | service.request.restore() 336 | t.end() 337 | }) 338 | }) 339 | 340 | test('get an error with no response body when trying to get statistics', function (t) { 341 | sinon.stub(service, 'request').callsFake(function (url, callback) { 342 | callback(new Error(), null) 343 | }) 344 | service.statistics('foo', [], function (err, count) { 345 | t.equal(err.code, 500) 346 | service.request.restore() 347 | t.end() 348 | }) 349 | }) 350 | 351 | test('time out when there is no response', function (t) { 352 | var error 353 | service.timeOut = 5 354 | nock('http://www.timeout.com').get('/').socketDelay(100).reply({}.toString()) 355 | 356 | service.request('http://www.timeout.com', function (err, data) { 357 | error = err 358 | }) 359 | setTimeout(function () { 360 | t.equal(typeof error, 'object') 361 | service.timeOut = 1000 362 | t.end() 363 | }, 25) 364 | }) 365 | 366 | test('should trigger catchErrors with an error when receiving json with an error in the response', function (t) { 367 | var data = { 368 | error: { 369 | code: 400, 370 | message: 'Invalid or missing input parameters.', 371 | details: [] 372 | } 373 | } 374 | 375 | var fixture = nock('http://www.error.com') 376 | fixture.get('/').reply(200, JSON.stringify(data)) 377 | 378 | sinon.stub(service, '_catchErrors').callsFake(function (task, err, url, callback) { 379 | callback(err) 380 | }) 381 | 382 | var task = {req: 'http://www.error.com'} 383 | 384 | service._requestFeatures(task, function (err, data) { 385 | t.notEqual(typeof err, 'undefined') 386 | t.equal(err.body.code, 400) 387 | t.equal(err.body.message, 'Invalid or missing input parameters.') 388 | service._catchErrors.restore() 389 | t.end() 390 | }) 391 | }) 392 | 393 | // feature request integration tests 394 | test('requesting a page of features', function (t) { 395 | var page = fs.createReadStream(__dirname + '/fixtures/page.json') 396 | var fixture = nock('http://servicesqa.arcgis.com') 397 | 398 | fixture.get('/97KLIFOSt5CxbiRI/arcgis/rest/services/QA_data_simple_point_5000/FeatureServer/0/query?outSR=4326&f=json&outFields=*&where=1=1&resultOffset=1000&resultRecordCount=1000&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=') 399 | .reply(200, function () { return page }) 400 | 401 | var service = new FeatureService('http://servicesqa.arcgis.com/97KLIFOSt5CxbiRI/arcgis/rest/services/QA_data_simple_point_5000/FeatureServer/0') 402 | var task = {req: 'http://servicesqa.arcgis.com/97KLIFOSt5CxbiRI/arcgis/rest/services/QA_data_simple_point_5000/FeatureServer/0/query?outSR=4326&f=json&outFields=*&where=1=1&resultOffset=1000&resultRecordCount=1000&geometry=&returnGeometry=true&returnZ=true&geometryPrecision='} 403 | 404 | service._requestFeatures(task, function (err, json) { 405 | t.error(err) 406 | t.equal(json.features.length, 1000) 407 | t.end() 408 | }) 409 | }) 410 | 411 | test('requesting a page of features that is gzipped', function (t) { 412 | var page = fs.createReadStream(__dirname + '/fixtures/page.json').pipe(zlib.createGzip()) 413 | var fixture = nock('http://servicesqa.arcgis.com') 414 | 415 | fixture.get('/97KLIFOSt5CxbiRI/arcgis/rest/services/QA_data_simple_point_5000/FeatureServer/0/query?outSR=4326&f=json&outFields=*&where=1=1&resultOffset=1000&resultRecordCount=1000&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=') 416 | .reply(200, function () { return page }, {'content-encoding': 'gzip'}) 417 | 418 | var service = new FeatureService('http://servicesqa.arcgis.com/97KLIFOSt5CxbiRI/arcgis/rest/services/QA_data_simple_point_5000/FeatureServer/0') 419 | var task = {req: 'http://servicesqa.arcgis.com/97KLIFOSt5CxbiRI/arcgis/rest/services/QA_data_simple_point_5000/FeatureServer/0/query?outSR=4326&f=json&outFields=*&where=1=1&resultOffset=1000&resultRecordCount=1000&geometry=&returnGeometry=true&returnZ=true&geometryPrecision='} 420 | 421 | service._requestFeatures(task, function (err, json) { 422 | t.error(err) 423 | t.equal(json.features.length, 1000) 424 | t.end() 425 | }) 426 | }) 427 | 428 | test('requesting a page of features that is deflate encoded', function (t) { 429 | var page = fs.createReadStream(__dirname + '/fixtures/page.json').pipe(zlib.createDeflate()) 430 | var fixture = nock('http://servicesqa.arcgis.com') 431 | 432 | fixture.get('/97KLIFOSt5CxbiRI/arcgis/rest/services/QA_data_simple_point_5000/FeatureServer/0/query?outSR=4326&f=json&outFields=*&where=1=1&resultOffset=1000&resultRecordCount=1000&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=') 433 | .reply(200, function () { return page }, {'content-encoding': 'deflate'}) 434 | 435 | var service = new FeatureService('http://servicesqa.arcgis.com/97KLIFOSt5CxbiRI/arcgis/rest/services/QA_data_simple_point_5000/FeatureServer/0') 436 | var task = {req: 'http://servicesqa.arcgis.com/97KLIFOSt5CxbiRI/arcgis/rest/services/QA_data_simple_point_5000/FeatureServer/0/query?outSR=4326&f=json&outFields=*&where=1=1&resultOffset=1000&resultRecordCount=1000&geometry=&returnGeometry=true&returnZ=true&geometryPrecision='} 437 | 438 | service._requestFeatures(task, function (err, json) { 439 | t.error(err) 440 | t.equal(json.features.length, 1000) 441 | t.end() 442 | }) 443 | }) 444 | 445 | test('requesting a page of features and getting an html response', function (t) { 446 | var page = Buffer.from('') 447 | var fixture = nock('http://servicesqa.arcgis.com') 448 | 449 | fixture.get('/97KLIFOSt5CxbiRJ/arcgis/rest/services/QA_data_simple_point_5000/FeatureServer/0/query?outSR=4326&f=json&outFields=*&where=1=1&resultOffset=1000&resultRecordCount=1000&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=') 450 | .times(4) 451 | .reply(200, function () { return page }) 452 | 453 | var service = new FeatureService('http://servicesqa.arcgis.com/97KLIFOSt5CxbiRJ/arcgis/rest/services/QA_data_simple_point_5000/FeatureServer/0', {backoff: 1}) 454 | var task = {req: 'http://servicesqa.arcgis.com/97KLIFOSt5CxbiRJ/arcgis/rest/services/QA_data_simple_point_5000/FeatureServer/0/query?outSR=4326&f=json&outFields=*&where=1=1&resultOffset=1000&resultRecordCount=1000&geometry=&returnGeometry=true&returnZ=true&geometryPrecision='} 455 | 456 | service._requestFeatures(task, function (err, json) { 457 | t.ok(err) 458 | t.equal(err.message, 'Paging aborted: Received HTML or plain text when expecting JSON') 459 | t.end() 460 | }) 461 | }) 462 | 463 | test('requesting a page of features and getting an html response tacked on', function (t) { 464 | var features = require('./fixtures/features.json') 465 | var page = Buffer.from(JSON.stringify(features) + "\n") 466 | var fixture = nock('http://servicesqa.arcgis.com') 467 | 468 | fixture.get('/97KLIFOSt5CxbiRK/arcgis/rest/services/QA_data_simple_point_5000/FeatureServer/0/query?outSR=4326&f=json&outFields=*&where=1=1&resultOffset=1000&resultRecordCount=1000&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=') 469 | .times(4) 470 | .reply(200, function () { return page }) 471 | 472 | var service = new FeatureService('http://servicesqa.arcgis.com/97KLIFOSt5CxbiRK/arcgis/rest/services/QA_data_simple_point_5000/FeatureServer/0', { backoff: 1 }) 473 | var task = { req: 'http://servicesqa.arcgis.com/97KLIFOSt5CxbiRK/arcgis/rest/services/QA_data_simple_point_5000/FeatureServer/0/query?outSR=4326&f=json&outFields=*&where=1=1&resultOffset=1000&resultRecordCount=1000&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=' } 474 | 475 | service._requestFeatures(task, function (err, json) { 476 | t.notOk(err) 477 | t.equal(json.features.length, 1) 478 | t.end() 479 | }) 480 | }) 481 | 482 | test('requesting a page of features and getting an empty response', function (t) { 483 | var fixture = nock('http://servicesqa.arcgis.com') 484 | 485 | fixture.get('/97KLIFOSt5CxbiRI/arcgis/rest/services/QA_data_simple_point_5000/FeatureServer/0/query?outSR=4326&f=json&outFields=*&where=1=1&resultOffset=1000&resultRecordCount=1000&geometry=&returnGeometry=true&returnZ=true&geometryPrecision=') 486 | .times(4) 487 | .reply(200, function () { return undefined }) 488 | 489 | var service = new FeatureService('http://servicesqa.arcgis.com/97KLIFOSt5CxbiRI/arcgis/rest/services/QA_data_simple_point_5000/FeatureServer/0', {backoff: 1}) 490 | var task = {req: 'http://servicesqa.arcgis.com/97KLIFOSt5CxbiRI/arcgis/rest/services/QA_data_simple_point_5000/FeatureServer/0/query?outSR=4326&f=json&outFields=*&where=1=1&resultOffset=1000&resultRecordCount=1000&geometry=&returnGeometry=true&returnZ=true&geometryPrecision='} 491 | 492 | service._requestFeatures(task, function (err, json) { 493 | t.ok(err) 494 | t.equal(err.message, 'Paging aborted: Failed to parse server response') 495 | t.end() 496 | }) 497 | }) 498 | 499 | // paging integration tests 500 | test('building pages for a service that supports pagination', function (t) { 501 | var countPaging = require('./fixtures/countPaging.json') 502 | var layerPaging = require('./fixtures/layerPaging.json') 503 | var features = require('./fixtures/features.json') 504 | var fixture = nock('http://services3.arcgis.com') 505 | 506 | fixture.get('/Infrastructure/Railroads_Rail_Crossings_INDOT/MapServer/0/query?where=1=1&returnCountOnly=true&f=json') 507 | .reply(200, countPaging) 508 | 509 | fixture.get('/Infrastructure/Railroads_Rail_Crossings_INDOT/MapServer/0?f=json') 510 | .reply(200, layerPaging) 511 | 512 | fixture.get('/Infrastructure/Railroads_Rail_Crossings_INDOT/MapServer/0/query?f=json&where=1=1&outFields=*&resultOffset=100&resultRecordCount=100') 513 | .reply(200, features) 514 | 515 | var service = new FeatureService('http://services3.arcgis.com/Infrastructure/Railroads_Rail_Crossings_INDOT/MapServer/0', {}) 516 | 517 | service.pages(function (err, pages) { 518 | t.equal(err, null) 519 | t.equal(pages.length, 156) 520 | t.end() 521 | }) 522 | }) 523 | 524 | test('building pages from a layer that does not support pagination', function (t) { 525 | var layerNoPaging = require('./fixtures/layerNoPaging.json') 526 | var countNoPaging = require('./fixtures/countNoPaging.json') 527 | var statsNoPaging = require('./fixtures/statsNoPaging.json') 528 | var fixture = nock('http://maps2.dcgis.dc.gov') 529 | 530 | fixture.get('/dcgis/rest/services/DCGIS_DATA/Transportation_WebMercator/MapServer/50/query?where=1=1&returnCountOnly=true&f=json') 531 | .reply(200, countNoPaging) 532 | 533 | fixture.get('/dcgis/rest/services/DCGIS_DATA/Transportation_WebMercator/MapServer/50?f=json') 534 | .reply(200, layerNoPaging) 535 | 536 | fixture.get('/dcgis/rest/services/DCGIS_DATA/Transportation_WebMercator/MapServer/50/query?f=json&outFields=&outStatistics=%5B%7B%22statisticType%22:%22min%22,%22onStatisticField%22:%22OBJECTID_1%22,%22outStatisticFieldName%22:%22min_OBJECTID_1%22%7D,%7B%22statisticType%22:%22max%22,%22onStatisticField%22:%22OBJECTID_1%22,%22outStatisticFieldName%22:%22max_OBJECTID_1%22%7D%5D') 537 | .reply(200, statsNoPaging) 538 | 539 | var service = new FeatureService('http://maps2.dcgis.dc.gov/dcgis/rest/services/DCGIS_DATA/Transportation_WebMercator/MapServer/50') 540 | service.pages(function (err, pages) { 541 | t.equal(err, null) 542 | t.equal(pages.length, 1) 543 | t.end() 544 | }) 545 | }) 546 | 547 | test('building pages for a service where metadata indicates supports pagination, but query response suggests otherwise', function (t) { 548 | var countFailedPaging = require('./fixtures/countFailedPaging.json') 549 | var layerPaging = require('./fixtures/layerPaging.json') 550 | var featuresEmpty = require('./fixtures/features-empty.json') 551 | var statsFailedPaging = require('./fixtures/statsFailedPaging.json') 552 | var fixture = nock('http://services3.arcgis.com') 553 | 554 | fixture.get('/Infrastructure/Railroads_Rail_Crossings_INDOT/MapServer/0/query?where=1=1&returnCountOnly=true&f=json') 555 | .reply(200, countFailedPaging) 556 | 557 | fixture.get('/Infrastructure/Railroads_Rail_Crossings_INDOT/MapServer/0?f=json') 558 | .reply(200, layerPaging) 559 | 560 | fixture.get('/Infrastructure/Railroads_Rail_Crossings_INDOT/MapServer/0/query?f=json&where=1=1&outFields=*&resultOffset=100&resultRecordCount=100') 561 | .reply(200, featuresEmpty) 562 | 563 | fixture.get('/Infrastructure/Railroads_Rail_Crossings_INDOT/MapServer/0/query?f=json&outFields=&outStatistics=%5B%7B%22statisticType%22:%22min%22,%22onStatisticField%22:%22OBJECTID%22,%22outStatisticFieldName%22:%22min_OBJECTID%22%7D,%7B%22statisticType%22:%22max%22,%22onStatisticField%22:%22OBJECTID%22,%22outStatisticFieldName%22:%22max_OBJECTID%22%7D%5D') 564 | .reply(200, statsFailedPaging) 565 | 566 | var service = new FeatureService('http://services3.arcgis.com/Infrastructure/Railroads_Rail_Crossings_INDOT/MapServer/0', {}) 567 | service.pages(function (err, pages) { 568 | t.equal(err, null) 569 | t.equal(pages.length, 156) 570 | t.end() 571 | }) 572 | }) 573 | 574 | test('building pages from a layer where statistics fail', function (t) { 575 | var layerStatsFail = require('./fixtures/layerStatsFail.json') 576 | var countStatsFail = require('./fixtures/countStatsFail.json') 577 | var idsStatsFail = require('./fixtures/idsStatsFail.json') 578 | var statsFail = require('./fixtures/statsFail.json') 579 | var fixture = nock('http://maps2.dcgis.dc.gov') 580 | 581 | fixture.get('/dcgis/rest/services/FEEDS/CDW_Feeds/MapServer/8/query?where=1=1&returnIdsOnly=true&f=json') 582 | .reply(200, idsStatsFail) 583 | 584 | fixture.get('/dcgis/rest/services/FEEDS/CDW_Feeds/MapServer/8/query?where=1=1&returnCountOnly=true&f=json') 585 | .reply(200, countStatsFail) 586 | 587 | fixture.get('/dcgis/rest/services/FEEDS/CDW_Feeds/MapServer/8?f=json') 588 | .reply(200, layerStatsFail) 589 | 590 | fixture.get('/dcgis/rest/services/FEEDS/CDW_Feeds/MapServer/8/query?f=json&outFields=&outStatistics=%5B%7B%22statisticType%22:%22min%22,%22onStatisticField%22:%22ESRI_OID%22,%22outStatisticFieldName%22:%22min_ESRI_OID%22%7D,%7B%22statisticType%22:%22max%22,%22onStatisticField%22:%22ESRI_OID%22,%22outStatisticFieldName%22:%22max_ESRI_OID%22%7D%5D') 591 | .reply(200, statsFail) 592 | 593 | var service = new FeatureService('http://maps2.dcgis.dc.gov/dcgis/rest/services/FEEDS/CDW_Feeds/MapServer/8') 594 | 595 | service.pages(function (err, pages) { 596 | t.equal(err, null) 597 | t.equal(pages.length, 4) 598 | t.end() 599 | }) 600 | }) 601 | 602 | test('building pages for a version 10.0 server', function (t) { 603 | var layer10 = require('./fixtures/layer10.0.json') 604 | var ids10 = require('./fixtures/ids10.0.json') 605 | var fixture = nock('http://sampleserver3.arcgisonline.com') 606 | 607 | fixture.get('/ArcGIS/rest/services/Fire/Sheep/FeatureServer/2?f=json').reply(200, layer10) 608 | 609 | fixture.get('/ArcGIS/rest/services/Fire/Sheep/FeatureServer/2/query?where=1=1&returnIdsOnly=true&f=json').reply(200, ids10) 610 | 611 | var service = new FeatureService('http://sampleserver3.arcgisonline.com/ArcGIS/rest/services/Fire/Sheep/FeatureServer/2') 612 | 613 | service.pages(function (err, pages) { 614 | t.equal(err, null) 615 | t.equal(pages.length, 1) 616 | t.end() 617 | }) 618 | }) 619 | 620 | test('service times out on third try for features', function (t) { 621 | var service = new FeatureService('http://www.foobar.com/FeatureServer', {timeOut: 5, layer: 0}) 622 | nock('http://www.foobar.com').get('/FeatureServer/0/query?where=1=1').socketDelay(100).reply({}.toString()) 623 | sinon.stub(service, '_abortPaging').callsFake(function (err, callback) { 624 | callback(err) 625 | }) 626 | 627 | var task = { 628 | retry: 3, 629 | req: 'http://www.foobar.com/FeatureServer/0/query?where=1=1' 630 | } 631 | service._requestFeatures(task, function (err) { 632 | t.equal(err.url, 'http://www.foobar.com/FeatureServer/0/query?where=1=1') 633 | t.end() 634 | }) 635 | }) 636 | 637 | test('catching errors with a json payload', function (t) { 638 | var service = new FeatureService('http://service.com/mapserver/2') 639 | var task = { retry: 3 } 640 | var error = new Error('Request for a page of features failed') 641 | var body = { 642 | code: 400, 643 | message: 'Invalid or missing input parameters', 644 | details: [] 645 | } 646 | error.body = body 647 | error.url = 'http://url.com' 648 | 649 | sinon.stub(service, '_abortPaging').callsFake(function (error, cb) { 650 | cb(error) 651 | }) 652 | 653 | service._catchErrors(task, error, error.url, function (info) { 654 | t.equal(info.message, 'Request for a page of features failed') 655 | t.equal(info.url, error.url) 656 | t.equal(info.body, body) 657 | service._abortPaging.restore() 658 | t.end() 659 | }) 660 | }) 661 | 662 | test('logging with a passed in logger', function (t) { 663 | var logger = {} 664 | t.plan(2) 665 | logger.log = function (level, message) { 666 | t.equal(level, 'test') 667 | t.equal(message, 'test') 668 | } 669 | var service = new FeatureService('htt://service.com/mapserver/3', {logger: logger}) 670 | service.log('test', 'test') 671 | }) 672 | 673 | test('logging without a passed in logger', function (t) { 674 | t.plan(2) 675 | sinon.stub(service, '_console').callsFake(function (level, message) { 676 | t.equal(level, 'test') 677 | t.equal(message, 'test') 678 | service._console.restore() 679 | }) 680 | 681 | service.log('test', 'test') 682 | }) 683 | 684 | test('setting concurrency for a hosted polygon service', function (t) { 685 | t.plan(1) 686 | var concurrency = Utils.setConcurrency(true, 'esriGeometryPolygon') 687 | t.equal(concurrency, 4) 688 | }) 689 | 690 | test('setting concurrency for an on-premise line service', function (t) { 691 | t.plan(1) 692 | var concurrency = Utils.setConcurrency(false, 'esriGeometryLine') 693 | t.equal(concurrency, 1) 694 | }) 695 | 696 | test('setting concurrency for a hosted point service', function (t) { 697 | t.plan(1) 698 | var concurrency = Utils.setConcurrency(true, 'esriGeometryPoint') 699 | t.equal(concurrency, 16) 700 | }) 701 | 702 | test('setting concurrency for an on-premise point service', function (t) { 703 | t.plan(1) 704 | var concurrency = Utils.setConcurrency(false, 'esriGeometryPoint') 705 | t.equal(concurrency, 4) 706 | }) 707 | 708 | test('url not parseable as mapserver or featureserver with layer should throw error', function (t) { 709 | try { 710 | Utils.parseUrl('http://example.com/') 711 | t.fail('an error should have been thrown') 712 | } catch (err) { 713 | t.ok(err instanceof TypeError) 714 | t.equals(err.message, 'unable to parse http://example.com/ as a mapserver or featureserver with optional layer') 715 | } 716 | t.end() 717 | }) --------------------------------------------------------------------------------