diff --git a/features/lib/osrm_loader.js b/features/lib/osrm_loader.js index 992eda433..e1db7803e 100644 --- a/features/lib/osrm_loader.js +++ b/features/lib/osrm_loader.js @@ -6,6 +6,7 @@ const Timeout = require('node-timeout'); const tryConnect = require('../lib/try_connect'); const errorReason = require('./utils').errorReason; +const child_process = require('child_process'); class OSRMBaseLoader{ constructor (scope) { this.scope = scope; @@ -45,7 +46,7 @@ class OSRMBaseLoader{ var retryCount = 0; let retry = (err) => { if (err) { - if (retryCount < 10) { + if (retryCount < 1000) { retryCount++; setTimeout(() => { tryConnect(this.scope.OSRM_PORT, retry); }, 10); } else { @@ -88,6 +89,33 @@ class OSRMDirectLoader extends OSRMBaseLoader { } }; +class ValhallaDirectLoader extends OSRMBaseLoader { + constructor (scope) { + super(scope); + } + + load (inputFile, callback) { + this.inputFile = inputFile; + this.shutdown(() => { + this.launch(callback); + }); + } + + osrmUp (callback) { + if (this.osrmIsRunning()) return callback(new Error("osrm-routed already running!")); + + var args = [this.inputFile, '1']; + this.child = child_process.execFile('/Users/danpat/mapbox/valhalla/valhalla_service', args, this.scope.environment, (err) => { + if (err && err.signal !== 'SIGINT') { + this.child = null; + throw new Error(util.format('valhalla_service %s: %s', errorReason(err), err.cmd)); + } + }); + setTimeout/ + callback(); + } +}; + class OSRMDatastoreLoader extends OSRMBaseLoader { constructor (scope) { super(scope); @@ -134,6 +162,7 @@ class OSRMLoader { this.scope = scope; this.sharedLoader = new OSRMDatastoreLoader(this.scope); this.directLoader = new OSRMDirectLoader(this.scope); + this.valhallaLoader = new ValhallaDirectLoader(this.scope); this.method = scope.DEFAULT_LOAD_METHOD; } @@ -150,6 +179,9 @@ class OSRMLoader { this.loader = this.directLoader; this.directLoader.load(inputFile, callback); }); + } else if (this.method === 'valhalla') { + this.loader = this.valhallaLoader; + this.valhallaLoader.load(inputFile, callback); } else { callback(new Error('*** Unknown load method ' + method)); } diff --git a/features/step_definitions/data.js b/features/step_definitions/data.js index f0760b643..1736b7c4c 100644 --- a/features/step_definitions/data.js +++ b/features/step_definitions/data.js @@ -309,6 +309,11 @@ module.exports = function () { callback(); }); + this.Given(/^data is loaded with valhalla$/, (callback) => { + this.osrmLoader.setLoadMethod('valhalla'); + callback(); + }); + this.Given(/^the HTTP method "([^"]*)"$/, (method, callback) => { this.httpMethod = method; callback(); diff --git a/features/support/cache.js b/features/support/cache.js index b9634d9b3..801044777 100644 --- a/features/support/cache.js +++ b/features/support/cache.js @@ -101,8 +101,10 @@ module.exports = function() { this.setupScenarioCache = (scenarioID) => { this.scenarioCacheFile = this.getScenarioCacheFile(this.featureCacheDirectory, scenarioID); + this.scenarioCacheFilePBF = this.getScenarioCacheFilePBF(this.featureCacheDirectory, scenarioID); this.processedCacheFile = this.getProcessedCacheFile(this.featureProcessedCacheDirectory, scenarioID); this.inputCacheFile = this.getInputCacheFile(this.featureProcessedCacheDirectory, scenarioID); + this.inputCacheFilePBF = this.getInputCacheFilePBF(this.featureProcessedCacheDirectory, scenarioID); this.rasterCacheFile = this.getRasterCacheFile(this.featureProcessedCacheDirectory, scenarioID); this.speedsCacheFile = this.getSpeedsCacheFile(this.featureProcessedCacheDirectory, scenarioID); this.penaltiesCacheFile = this.getPenaltiesCacheFile(this.featureProcessedCacheDirectory, scenarioID); @@ -176,6 +178,11 @@ module.exports = function() { return path.join(featureCacheDirectory, scenarioID) + '.osm'; }; + // test/cache/{feature_path}/{feature_hash}/{scenario}.osm + this.getScenarioCacheFilePBF = (featureCacheDirectory, scenarioID) => { + return path.join(featureCacheDirectory, scenarioID) + '.pbf'; + }; + // test/cache/{feature_path}/{feature_hash}/{osrm_hash}/ this.getFeatureProcessedCacheDirectory = (featureCacheDirectory, osrmHash) => { return path.join(featureCacheDirectory, osrmHash); @@ -191,6 +198,11 @@ module.exports = function() { return path.join(featureProcessedCacheDirectory, scenarioID) + '.osm'; }; + // test/cache/{feature_path}/{feature_hash}/{osrm_hash}/{scenario}.osm + this.getInputCacheFilePBF = (featureProcessedCacheDirectory, scenarioID) => { + return path.join(featureProcessedCacheDirectory, scenarioID) + '.pbf'; + }; + return this; }; diff --git a/features/support/data.js b/features/support/data.js index e9e1f06f4..233bd1394 100644 --- a/features/support/data.js +++ b/features/support/data.js @@ -10,6 +10,8 @@ const tableDiff = require('../lib/table_diff'); const ensureDecimal = require('../lib/utils').ensureDecimal; const errorReason = require('../lib/utils').errorReason; +const child_process = require('child_process'); + module.exports = function () { this.setGridSize = (meters) => { // the constant is calculated (with BigDecimal as: 1.0/(DEG_TO_RAD*EARTH_RADIUS_IN_METERS @@ -168,7 +170,13 @@ module.exports = function () { if (exists) callback(); else { this.OSMDB.toXML((xml) => { - fs.writeFile(this.scenarioCacheFile, xml, callback); + fs.writeFile(this.scenarioCacheFile, xml, (err) => { + if (err) callback(err); + var params = ['cat',this.scenarioCacheFile,'-O','-o',this.scenarioCacheFilePBF, '--no-progress']; + let child = child_process.execFile('/usr/local/bin/osmium', params, {maxBuffer: 1024 * 1024 * 1000, env: {}}, (err,stdout,stderr) => { + callback(err); + }); + }); }); } }); @@ -178,7 +186,10 @@ module.exports = function () { fs.exists(this.inputCacheFile, (exists) => { if (exists) callback(); else { - fs.link(this.scenarioCacheFile, this.inputCacheFile, callback); + fs.link(this.scenarioCacheFile, this.inputCacheFile, (err) => { + if (err) callback(err); + fs.link(this.scenarioCacheFilePBF, this.inputCacheFilePBF, callback); + }); } }); }; @@ -239,23 +250,93 @@ module.exports = function () { }); }; + this.valhallaBuildConfig = (p, callback) => { + let stamp = p.processedCacheFile + '.stamp_valhalla_config'; + fs.exists(stamp, (exists) => { + if (exists) return callback(); + + // valhalla_build_config --mjolnir-tile-dir ${PWD}/valhalla_tiles + // --mjolnir-tile-extract ${PWD}/valhalla_tiles.tar + // --mjolnir-timezone ${PWD}/valhalla_tiles/timezones.sqlite + // --mjolnir-admin ${PWD}/valhalla_tiles/admins.sqlite > valhalla.json + + var params = [`--mjolnir-tile-dir`, `${p.inputCacheDir}/${p.scenarioID}_valhalla_tiles`, + `--mjolnir-tile-extract`, `${p.inputCacheDir}/${p.scenarioID}_valhalla_tiles.tar`, + `--mjolnir-timezone`, `${p.inputCacheDir}/${p.scenarioID}_valhalla_tiles/timezones.sqlite`, + `--mjolnir-admin`, `${p.inputCacheDir}/${p.scenarioID}_valhalla_tiles/admins.sqlite`]; + + child_process.execFile('/Users/danpat/mapbox/valhalla/scripts/valhalla_build_config', params, {}, (error, stdout, stderr) => { + if (error) { throw error; } + fs.writeFile(`${p.inputCacheDir}/${p.scenarioID}_valhalla_config.json`, stdout, (err) => { + if (err) throw err; + fs.writeFile(stamp, 'ok', callback); + }) + }); + }); + }; + + this.valhallaBuildTiles = (p, callback) => { + let stamp = p.processedCacheFile + '.stamp_valhalla_tiles'; + fs.exists(stamp, (exists) => { + if (exists) return callback(); + + var params = [`-c`,`${p.inputCacheDir}/${p.scenarioID}_valhalla_config.json`, + `${p.inputCacheFilePBF}`]; + + child_process.execFile('/Users/danpat/mapbox/valhalla/valhalla_build_tiles', params, {}, (error, stdout, stderr) => { + if (error) { throw error; } + console.log(stdout); + console.log(stderr); + fs.writeFile(stamp, 'ok', callback); + }); + }); + + }; + + this.valhallaTarTiles = (p, callback) => { + let stamp = p.processedCacheFile + '.stamp_valhalla_tartiles'; + fs.exists(stamp, (exists) => { + if (exists) return callback(); + var params = [`cf`,`${p.inputCacheDir}/${p.scenarioID}_valhalla_tiles.tar`, + `-C`, p.inputCacheDir, `${p.scenarioID}_valhalla_tiles`]; + + child_process.execFile('/usr/bin/tar', params, {}, (error, stdout, stderr) => { + if (error) { throw error; } + console.log(stdout); + console.log(stderr); + fs.writeFile(stamp, 'ok', callback); + }); + + }); + } + this.extractContractPartitionAndCustomize = (callback) => { // a shallow copy of scenario parameters to avoid data inconsistency // if a cucumber timeout occurs during deferred jobs let p = {extractArgs: this.extractArgs, contractArgs: this.contractArgs, partitionArgs: this.partitionArgs, customizeArgs: this.customizeArgs, profileFile: this.profileFile, inputCacheFile: this.inputCacheFile, - processedCacheFile: this.processedCacheFile, environment: this.environment}; + inputCacheFilePBF: this.inputCacheFilePBF, + processedCacheFile: this.processedCacheFile, environment: this.environment, + inputCacheDir: this.featureProcessedCacheDirectory, + scenarioID: this.scenarioID }; let queue = d3.queue(1); queue.defer(this.extractData.bind(this), p); queue.defer(this.partitionData.bind(this), p); queue.defer(this.contractData.bind(this), p); - queue.defer(this.customizeData.bind(this), p); + queue.defer(this.valhallaBuildConfig.bind(this), p); + queue.defer(this.valhallaBuildTiles.bind(this), p); + queue.defer(this.valhallaTarTiles.bind(this), p); queue.awaitAll(callback); }; this.writeAndLinkOSM = (callback) => { let queue = d3.queue(1); + let p = { profileFile: this.profileFile, inputCacheFile: this.inputCacheFile, + inputCacheFilePBF: this.inputCacheFilePBF, + processedCacheFile: this.processedCacheFile, environment: this.environment, + inputCacheDir: this.featureProcessedCacheDirectory, + scenarioID: this.scenarioID }; queue.defer(this.writeOSM.bind(this)); queue.defer(this.linkOSM.bind(this)); queue.awaitAll(callback); @@ -272,7 +353,8 @@ module.exports = function () { let queue = d3.queue(1); queue.defer(this.writeAndLinkOSM.bind(this)); queue.defer(this.extractContractPartitionAndCustomize.bind(this)); - queue.defer(this.osrmLoader.load.bind(this.osrmLoader), this.processedCacheFile); + //queue.defer(this.osrmLoader.load.bind(this.osrmLoader), this.processedCacheFile); + queue.defer(this.osrmLoader.load.bind(this.osrmLoader), `${this.featureProcessedCacheDirectory}/${this.scenarioID}_valhalla_config.json`); queue.awaitAll(callback); }; diff --git a/features/support/env.js b/features/support/env.js index 93bfdc635..3ba270c7c 100644 --- a/features/support/env.js +++ b/features/support/env.js @@ -31,7 +31,7 @@ module.exports = function () { this.DEFAULT_ENVIRONMENT = Object.assign({STXXLCFG: stxxl_config}, process.env); this.DEFAULT_PROFILE = 'bicycle'; this.DEFAULT_INPUT_FORMAT = 'osm'; - this.DEFAULT_LOAD_METHOD = 'datastore'; + this.DEFAULT_LOAD_METHOD = process.env.OSRM_LOAD_METHOD && process.env.OSRM_LOAD_METHOD || 'datastore'; this.DEFAULT_ORIGIN = [1,1]; this.OSM_USER = 'osrm'; this.OSM_UID = 1; diff --git a/features/support/http.js b/features/support/http.js index 71f61761a..9e3704f0b 100644 --- a/features/support/http.js +++ b/features/support/http.js @@ -22,22 +22,35 @@ module.exports = function () { // FIXME this needs to be simplified! // - remove usage of node-timeout // - replace with node's native timout mechanism - this.sendRequest = (baseUri, parameters, callback) => { + this.sendRequest = (baseUri, parameters, type, callback) => { var limit = Timeout(this.TIMEOUT, { err: { statusCode: 408 } }); var runRequest = (cb) => { - var params = this.paramsToString(parameters); - this.query = baseUri + (params.length ? '/' + params : ''); - request(this.query, (err, res, body) => { - if (err && err.code === 'ECONNREFUSED') { - return cb(new Error('*** osrm-routed is not running.')); - } else if (err && err.statusCode === 408) { - return cb(new Error()); - } + if (type === 'GET') { + var params = this.paramsToString(parameters); + this.query = baseUri + (params.length ? '/' + params : ''); + request(this.query, (err, res, body) => { + if (err && err.code === 'ECONNREFUSED') { + return cb(new Error('*** osrm-routed is not running.')); + } else if (err && err.statusCode === 408) { + return cb(new Error()); + } - return cb(err, res, body); - }); + return cb(err, res, body); + }); + } else { + this.query = baseUri; + request.post(this.query, {body: JSON.stringify(parameters)}, (err, res, body) => { + if (err && err.code === 'ECONNREFUSED') { + return cb(new Error('*** osrm-routed is not running.')); + } else if (err && err.statusCode === 408) { + return cb(new Error()); + } + + return cb(err, res, body); + }); + } }; runRequest(limit((err, res, body) => { diff --git a/features/support/route.js b/features/support/route.js index 739bc46af..6f59d315b 100644 --- a/features/support/route.js +++ b/features/support/route.js @@ -7,13 +7,13 @@ const ensureDecimal = require('../lib/utils').ensureDecimal; module.exports = function () { this.requestPath = (service, params, callback) => { var uri; - if (service == 'timestamp') { + if (service == 'timestamp' || this.osrmLoader.method === 'valhalla') { uri = [this.HOST, service].join('/'); } else { uri = [this.HOST, service, 'v1', this.profile].join('/'); } - return this.sendRequest(uri, params, callback); + return this.sendRequest(uri, params, this.osrmLoader.method === 'valhalla' ? 'POST' : 'GET', callback); }; this.requestUrl = (path, callback) => { @@ -71,6 +71,17 @@ module.exports = function () { if (approaches.length) { params.approaches = approaches.join(';'); } + + if (this.osrmLoader.method === 'valhalla') { + params = { + locations: waypoints.map(w => {return{ lat: w.lat, lon: w.lon };}), + costing: 'auto', + directions_options:{ + units:'miles'} + }; + } + + return this.requestPath('route', params, callback); }; @@ -126,10 +137,18 @@ module.exports = function () { }; this.extractInstructionList = (instructions, keyFinder) => { - if (instructions) { - return instructions.legs.reduce((m, v) => m.concat(v.steps), []) - .map(keyFinder) - .join(','); + if (this.osrmLoader.method === 'valhalla') { + if (instructions) { + return instructions.legs.reduce((m, v) => m.concat(v.maneuvers), []) + .map(keyFinder) + .join(','); + } + } else { + if (instructions) { + return instructions.legs.reduce((m, v) => m.concat(v.steps), []) + .map(keyFinder) + .join(','); + } } }; @@ -140,7 +159,13 @@ module.exports = function () { }; this.wayList = (instructions) => { - return this.extractInstructionList(instructions, s => s.name); + if (this.osrmLoader.method === 'valhalla') { + var result = this.extractInstructionList(instructions, s => s.street_names && s.street_names.join(',') || ''); + var laststep = result.split(',').slice(-2)[0]; + return result + laststep; + } else { + return this.extractInstructionList(instructions, s => s.name); + } }; this.refList = (instructions) => { @@ -166,12 +191,20 @@ module.exports = function () { }; this.bearingList = (instructions) => { - return this.extractInstructionList(instructions, s => ('in' in s.intersections[0] ? this.reverseBearing(s.intersections[0].bearings[s.intersections[0].in]) : 0) + + if (this.osrmLoader.method === 'valhalla') { + return 'NOT IMPLEMENTED'; + } else { + return this.extractInstructionList(instructions, s => ('in' in s.intersections[0] ? this.reverseBearing(s.intersections[0].bearings[s.intersections[0].in]) : 0) + '->' + ('out' in s.intersections[0] ? s.intersections[0].bearings[s.intersections[0].out] : 0)); + } }; this.lanesList = (instructions) => { + if (this.osrmLoader.method === 'valhalla') { + return 'NOT IMPLEMENTED'; + } else { return this.extractInstructionList(instructions, s => { return s.intersections.map( i => { if(i.lanes) @@ -187,6 +220,7 @@ module.exports = function () { } }).join(';'); }); + } }; this.approachList = (instructions) => { @@ -216,6 +250,89 @@ module.exports = function () { }; this.turnList = (instructions) => { + if (this.osrmLoader.method === 'valhalla') { + /* + from https://github.com/valhalla/valhalla-docs/blob/master/turn-by-turn/api-reference.md + + kNone = 0; +kStart = 1; kStartRight = 2; kStartLeft = 3; kDestination = 4; kDestinationRight = 5; kDestinationLeft = 6; +kBecomes = 7; kContinue = 8; +kSlightRight = 9; kRight = 10; kSharpRight = 11; kUturnRight = 12; + +kUturnLeft = 13; kSharpLeft = 14; kLeft = 15; kSlightLeft = 16; + +kRampStraight = 17; kRampRight = 18; kRampLeft = 19; + +kExitRight = 20; kExitLeft = 21; + +kStayStraight = 22; kStayRight = 23; kStayLeft = 24; + +kMerge = 25; + +kRoundaboutEnter = 26; +kRoundaboutExit = 27; +kFerryEnter = 28; +kFerryExit = 29; +kTransit = 30; +kTransitTransfer = 31; +kTransitRemainOn = 32; +kTransitConnectionStart = 33; +kTransitConnectionTransfer = 34; +kTransitConnectionDestination = 35; +kPostTransitConnectionDestination = 36; +*/ +var typemap = { + 1: {type: 'depart', modifier: null }, + 2: {type: 'depart', modifier: 'right' }, + 3: {type: 'depart', modifier: 'left' }, + + 4: {type: 'arrive', modifier: null }, + 5: {type: 'arrive', modifier: 'right' }, + 6: {type: 'arrive', modifier: 'left' }, + + 7: {type: 'becomes', modifier: null }, + 8: {type: 'continue', modifier: 'straight' }, + + 9: {type: 'turn', modifier: 'slight right' }, + 10: {type: 'turn', modifier: 'right' }, + 11: {type: 'turn', modifier: 'sharp right' }, + 12: {type: 'uturn', modifier: 'right' }, + + 13: {type: 'uturn', modifier: 'left' }, + 14: {type: 'turn', modifier: 'sharp left' }, + 15: {type: 'turn', modifier: 'left' }, + 16: {type: 'turn', modifier: 'slight left' }, + + 17: {type: 'on ramp', modifier: 'straight' }, + 18: {type: 'on ramp', modifier: 'right' }, + 19: {type: 'on ramp', modifier: 'left' }, + + 20: {type: 'off ramp', modifier: 'right' }, + 21: {type: 'off ramp', modifier: 'left' }, + + 22: {type: 'continue', modifier: 'straight' }, + 23: {type: 'continue', modifier: 'right' }, + 24: {type: 'continue', modifier: 'left' }, + + 25: {type: 'merge', modifier: null }, + + 26: {type: 'roundabout', modifier: null }, + 27: {type: 'roundabout-exit', modifier: null }, + +} + return instructions.legs.reduce((m, v) => m.concat(v.maneuvers), []) + .map(v => { + if (v.type in typemap) { + if (typemap[v.type].modifier) { + return `${typemap[v.type].type} ${typemap[v.type].modifier}`; + } else { + return `${typemap[v.type].type}`; + } + } else { + return "UNRECOGNIZED"; + } + }); + } else { return instructions.legs.reduce((m, v) => m.concat(v.steps), []) .map(v => { switch (v.maneuver.type) { @@ -240,17 +357,25 @@ module.exports = function () { } }) .join(','); + } }; this.locations = (instructions) => { + if (this.osrmLoader.method === 'valhalla') { + return 'NOT IMPLEMENTED'; + } else { return instructions.legs.reduce((m, v) => m.concat(v.steps), []) .map(v => { return this.findNodeByLocation(v.maneuver.location); }) .join(','); + } }; this.intersectionList = (instructions) => { + if (this.osrmLoader.method === 'valhalla') { + return 'NOT IMPLEMENTED'; + } else { return instructions.legs.reduce((m, v) => m.concat(v.steps), []) .map( v => { return v.intersections @@ -261,6 +386,7 @@ module.exports = function () { return string; }).join(','); }).join(';'); + } }; this.modeList = (instructions) => { @@ -272,7 +398,11 @@ module.exports = function () { }; this.classesList = (instructions) => { + if (this.osrmLoader.method === 'valhalla') { + return 'NOT IMPLEMENTED'; + } else { return this.extractInstructionList(instructions, s => '[' + s.intersections.map(i => '(' + (i.classes ? i.classes.join(',') : '') + ')').join(',') + ']'); + } }; this.timeList = (instructions) => { diff --git a/features/support/shared_steps.js b/features/support/shared_steps.js index fa98fe120..0aa28db50 100644 --- a/features/support/shared_steps.js +++ b/features/support/shared_steps.js @@ -40,31 +40,42 @@ module.exports = function () { let json = JSON.parse(body); - got.code = json.code; - - let hasRoute = json.code === 'Ok'; + var hasRoute = false; + if (this.osrmLoader.method === 'valhalla') { + got.code = json.trip.status; + hasRoute = json.trip.status == 0; + } else { + got.code = json.code; + hasRoute = json.code === 'Ok'; + } + var route; if (hasRoute) { - instructions = this.wayList(json.routes[0]); - pronunciations = this.pronunciationList(json.routes[0]); - refs = this.refList(json.routes[0]); - destinations = this.destinationsList(json.routes[0]); - exits = this.exitsList(json.routes[0]); - bearings = this.bearingList(json.routes[0]); - turns = this.turnList(json.routes[0]); - intersections = this.intersectionList(json.routes[0]); - modes = this.modeList(json.routes[0]); - driving_sides = this.drivingSideList(json.routes[0]); - classes = this.classesList(json.routes[0]); - times = this.timeList(json.routes[0]); - distances = this.distanceList(json.routes[0]); - lanes = this.lanesList(json.routes[0]); - summary = this.summary(json.routes[0]); - locations = this.locations(json.routes[0]); - annotation = this.annotationList(json.routes[0]); - weight_name = this.weightName(json.routes[0]); - weights = this.weightList(json.routes[0]); - approaches = this.approachList(json.routes[0]); + if (this.osrmLoader.method === 'valhalla') { + route = json.trip; + } else { + route = json.routes[0]; + } + instructions = this.wayList(route); + pronunciations = this.pronunciationList(route); + refs = this.refList(route); + destinations = this.destinationsList(route); + exits = this.exitsList(route); + bearings = this.bearingList(route); + turns = this.turnList(route); + intersections = this.intersectionList(route); + modes = this.modeList(route); + driving_sides = this.drivingSideList(route); + classes = this.classesList(route); + times = this.timeList(route); + distances = this.distanceList(route); + lanes = this.lanesList(route); + summary = this.summary(route); + locations = this.locations(route); + annotation = this.annotationList(route); + weight_name = this.weightName(route); + weights = this.weightList(route); + approaches = this.approachList(route); } if (headers.has('status')) { @@ -99,9 +110,9 @@ module.exports = function () { got.alternative = this.wayList(json.routes[1]); } - var distance = hasRoute && json.routes[0].distance, - time = hasRoute && json.routes[0].duration, - weight = hasRoute && json.routes[0].weight; + var distance = hasRoute && (this.osrmLoader.method === 'valhalla') ? route.summary.length : route.distance, + time = hasRoute && (this.osrmLoader.method === 'valhalla') ? route.summary.time : route.duration, + weight = hasRoute && (this.osrmLoader.method === 'valhalla') ? route.summary.time : route.weight; if (headers.has('distance')) { if (row.distance.length) {