osrm-backend/features/support/data.js
Michael Bell db7946d762
Add support for disabling feature datasets (#6666)
This change adds support for disabling datasets, such that specific
files are not loaded into memory when running OSRM. This enables users
to not pay the memory cost for features they do not intend to use.

Initially, there are two options:
- ROUTE_GEOMETRY, for disabling overview, steps, annotations and waypoints.
- ROUTE_STEPS, for disabling steps only.

Attempts to query features for which the datasets are disabled will
lead to a DisabledDatasetException being returned.
2023-08-04 18:43:37 +01:00

304 lines
11 KiB
JavaScript

'use strict';
const fs = require('fs');
const util = require('util');
const d3 = require('d3-queue');
const OSM = require('../lib/osm');
const classes = require('./data_classes');
const tableDiff = require('../lib/table_diff');
const ensureDecimal = require('../lib/utils').ensureDecimal;
const errorReason = require('../lib/utils').errorReason;
const CheapRuler = require('cheap-ruler');
module.exports = function () {
this.setGridSize = (meters) => {
this.gridSize = parseFloat(meters);
// the constant is calculated (with BigDecimal as: 1.0/(DEG_TO_RAD*EARTH_RADIUS_IN_METERS
// see ApproximateDistance() in ExtractorStructs.h
// it's only accurate when measuring along the equator, or going exactly north-south
this.zoom = this.gridSize * 0.8990679362704610899694577444566908445396483347536032203503E-5;
};
this.setOrigin = (origin) => {
this.origin = origin;
// we use C++ version of `cheap-ruler` inside OSRM in order to do distance calculations,
// so here we use it too to have a bit more precise assertions
this.ruler = new CheapRuler(this.origin[1], 'meters');
};
this.offsetOriginBy = (xCells, yCells) => {
return this.ruler.offset(this.origin, xCells * this.gridSize, yCells * this.gridSize);
};
this.buildWaysFromTable = (table, callback) => {
// add one unconnected way for each row
var buildRow = (row, ri, cb) => {
// comments ported directly from ruby suite:
// NOTE: currently osrm crashes when processing an isolated oneway with just 2 nodes, so we use 4 edges
// this is related to the fact that a oneway dead-end street doesn't make a lot of sense
// if we stack ways on different x coordinates, routability tests get messed up, because osrm might pick a neighboring way if the one test can't be used.
// instead we place all lines as a string on the same y coordinate. this prevents using neighboring ways.
// add some nodes
var makeFakeNode = (namePrefix, offset) => {
const coord = this.offsetOriginBy(offset + this.WAY_SPACING * ri, 0);
return new OSM.Node(this.makeOSMId(), this.OSM_USER, this.OSM_TIMESTAMP,
this.OSM_UID, coord[0],
coord[1], {name: util.format('%s%d', namePrefix, ri)});
};
var nodes = ['a','b','c','d','e'].map((l, i) => makeFakeNode(l, i));
nodes.forEach(node => { this.OSMDB.addNode(node); });
// ...with a way between them
var way = new OSM.Way(this.makeOSMId(), this.OSM_USER, this.OSM_TIMESTAMP, this.OSM_UID);
nodes.forEach(node => { way.addNode(node); });
// remove tags that describe expected test result, reject empty tags
var tags = {};
for (var rkey in row) {
if (!rkey.match(/^forw\b/) &&
!rkey.match(/^backw\b/) &&
!rkey.match(/^bothw\b/) &&
row[rkey].length)
tags[rkey] = row[rkey];
}
var wayTags = { highway: 'primary' },
nodeTags = {};
for (var key in tags) {
var nodeMatch = key.match(/node\/(.*)/);
if (nodeMatch) {
if (tags[key] === '(nil)') {
delete nodeTags[key];
} else {
nodeTags[nodeMatch[1]] = tags[key];
}
} else {
if (tags[key] === '(nil)') {
delete wayTags[key];
} else {
wayTags[key] = tags[key];
}
}
}
wayTags.name = util.format('w%d', ri);
way.setTags(wayTags);
this.OSMDB.addWay(way);
for (var k in nodeTags) {
nodes[2].addTag(k, nodeTags[k]);
}
cb();
};
var q = d3.queue();
table.hashes().forEach((row, ri) => {
q.defer(buildRow, row, ri);
});
q.awaitAll(callback);
};
this.tableCoordToLonLat = (ci, ri) => {
return this.offsetOriginBy(ci, -ri).map(ensureDecimal);
};
this.addOSMNode = (name, lon, lat, id) => {
id = id || this.makeOSMId();
var node = new OSM.Node(id, this.OSM_USER, this.OSM_TIMESTAMP, this.OSM_UID, lon, lat, {name: name});
this.OSMDB.addNode(node);
this.nameNodeHash[name] = node;
};
this.addLocation = (name, lon, lat) => {
this.locationHash[name] = new classes.Location(lon, lat);
};
this.findNodeByName = (s) => {
if (s.length !== 1) throw new Error(util.format('*** invalid node name "%s", must be single characters', s));
if (!s.match(/[a-z0-9]/)) throw new Error(util.format('*** invalid node name "%s", must be alphanumeric', s));
var fromNode;
if (s.match(/[a-z]/)) {
fromNode = this.nameNodeHash[s.toString()];
} else {
fromNode = this.locationHash[s.toString()];
}
return fromNode;
};
// find a node based on an array containing lon/lat
this.findNodeByLocation = (node_location) => {
var searched_coordinate = new classes.Location(node_location[0],node_location[1]);
for (var node in this.nameNodeHash)
{
var node_coordinate = new classes.Location(this.nameNodeHash[node].lon,this.nameNodeHash[node].lat);
if (this.FuzzyMatch.matchCoordinate(searched_coordinate, node_coordinate, this.zoom))
{
return node;
}
}
return '_';
};
this.findWayByName = (s) => {
return this.nameWayHash[s.toString()] || this.nameWayHash[s.toString().split('').reverse().join('')];
};
this.findRelationByName = (s) => {
return this.nameRelationHash[s.toString()] || this.nameRelationHash[s.toString().split('').reverse().join('')];
};
this.makeOSMId = () => {
this.osmID = this.osmID + 1;
return this.osmID;
};
this.resetOSM = () => {
this.OSMDB.clear();
this.nameNodeHash = {};
this.locationHash = {};
this.shortcutsHash = {};
this.nameWayHash = {};
this.nameRelationHash = {};
this.osmID = 0;
};
this.writeOSM = (callback) => {
fs.exists(this.scenarioCacheFile, (exists) => {
if (exists) callback();
else {
this.OSMDB.toXML((xml) => {
fs.writeFile(this.scenarioCacheFile, xml, callback);
});
}
});
};
this.linkOSM = (callback) => {
fs.exists(this.inputCacheFile, (exists) => {
if (exists) callback();
else {
fs.link(this.scenarioCacheFile, this.inputCacheFile, callback);
}
});
};
this.extractData = (p, callback) => {
let stamp = p.processedCacheFile + '.stamp_extract';
fs.exists(stamp, (exists) => {
if (exists) return callback();
this.runBin('osrm-extract', util.format('%s --profile %s %s', p.extractArgs, p.profileFile, p.inputCacheFile), p.environment, (err) => {
if (err) {
return callback(new Error(util.format('osrm-extract %s: %s', errorReason(err), err.cmd)));
}
fs.writeFile(stamp, 'ok', callback);
});
});
};
this.contractData = (p, callback) => {
let stamp = p.processedCacheFile + '.stamp_contract';
fs.exists(stamp, (exists) => {
if (exists) return callback();
this.runBin('osrm-contract', util.format('%s %s', p.contractArgs, p.processedCacheFile), p.environment, (err) => {
if (err) {
return callback(new Error(util.format('osrm-contract %s: %s', errorReason(err), err)));
}
fs.writeFile(stamp, 'ok', callback);
});
});
};
this.partitionData = (p, callback) => {
let stamp = p.processedCacheFile + '.stamp_partition';
fs.exists(stamp, (exists) => {
if (exists) return callback();
this.runBin('osrm-partition', util.format('%s %s', p.partitionArgs, p.processedCacheFile), p.environment, (err) => {
if (err) {
return callback(new Error(util.format('osrm-partition %s: %s', errorReason(err), err.cmd)));
}
fs.writeFile(stamp, 'ok', callback);
});
});
};
this.customizeData = (p, callback) => {
let stamp = p.processedCacheFile + '.stamp_customize';
fs.exists(stamp, (exists) => {
if (exists) return callback();
this.runBin('osrm-customize', util.format('%s %s', p.customizeArgs, p.processedCacheFile), p.environment, (err) => {
if (err) {
return callback(new Error(util.format('osrm-customize %s: %s', errorReason(err), err)));
}
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};
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.awaitAll(callback);
};
this.writeAndLinkOSM = (callback) => {
let queue = d3.queue(1);
queue.defer(this.writeOSM.bind(this));
queue.defer(this.linkOSM.bind(this));
queue.awaitAll(callback);
};
this.reprocess = (callback) => {
let queue = d3.queue(1);
queue.defer(this.writeAndLinkOSM.bind(this));
queue.defer(this.extractContractPartitionAndCustomize.bind(this));
queue.awaitAll(callback);
};
this.reprocessAndLoadData = (callback) => {
let p = {loaderArgs: this.loaderArgs, inputFile: this.processedCacheFile};
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), p);
queue.awaitAll(callback);
};
this.processRowsAndDiff = (table, fn, callback) => {
var q = d3.queue(1);
table.hashes().forEach((row, i) => { q.defer(fn, row, i); });
q.awaitAll((err, actual) => {
if (err) return callback(err);
let diff = tableDiff(table, actual);
if (diff) callback(diff);
else callback();
});
};
};