UNPKG

sharedstreets

Version:

SharedStreets, a 'digital commons' for the street

986 lines (985 loc) 59 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Graph = exports.LevelDB = exports.PathCandidate = exports.PathSegment = exports.PointCandidate = exports.ReferenceSideOfStreet = exports.ReferenceDirection = exports.GraphMode = exports.MatchType = void 0; const turfHelpers = __importStar(require("@turf/helpers")); const tiles_1 = require("./tiles"); const fs_1 = require("fs"); const tile_index_1 = require("./tile_index"); const index_1 = require("./index"); const child_process_1 = require("child_process"); const length_1 = __importDefault(require("@turf/length")); const distance_1 = __importDefault(require("@turf/distance")); const along_1 = __importDefault(require("@turf/along")); const bearing_1 = __importDefault(require("@turf/bearing")); const nearest_point_on_line_1 = __importDefault(require("@turf/nearest-point-on-line")); const fs = __importStar(require("fs")); const util_1 = require("./util"); const { exec } = require('child_process'); const OSRM = require("osrm"); const xml = require('xml'); const stream = require('stream'); const levelup = require('levelup'); const leveldown = require('leveldown'); const chalk = require('chalk'); const path = require('path'); const util = require('util'); const uuidHash = require('uuid-by-string'); const DEFAULT_SEARCH_RADIUS = 10; const MIN_CONFIDENCE = 0.5; const OPTIMIZE_GRAPH = true; const USE_LOCAL_CACHE = true; const SHST_GRAPH_CACHE_DIR = util_1.resolveHome('~/.shst/cache/graphs/'); function getOSRMDirectory() { const osrmPath = require.resolve('osrm'); const osrmLibPath = path.dirname(osrmPath); const osrmDirPath = path.join(osrmLibPath, '..'); if (fs.existsSync(osrmDirPath)) { return osrmDirPath; } else return null; } const OSRM_DIR = getOSRMDirectory(); var MatchType; (function (MatchType) { MatchType["DIRECT"] = "direct"; MatchType["HMM"] = "hmm"; })(MatchType = exports.MatchType || (exports.MatchType = {})); var GraphMode; (function (GraphMode) { GraphMode["CAR_ALL"] = "car_all"; GraphMode["CAR_SURFACE_ONLY"] = "car_surface_only"; GraphMode["CAR_MOTORWAY_ONLY"] = "car_motorway_only"; GraphMode["BIKE"] = "bike"; GraphMode["PEDESTRIAN"] = "ped"; })(GraphMode = exports.GraphMode || (exports.GraphMode = {})); // interface typecheck for SharedStreetsGeometry function geomInstance(object) { return 'forwardReferenceId' in object; } function getReferenceLength(ref) { var length = 0; for (var lr of ref.locationReferences) { if (lr.distanceToNextRef) length += lr.distanceToNextRef; } return length / 100; } const DEFAULT_LENGTH_TOLERANCE = 0.1; const DEFUALT_CANDIDATES = 10; const DEFAULT_BEARING_TOLERANCE = 15; // 360 +/- tolerance const MAX_FEATURE_LENGTH = 15000; // 1km const MAX_SEARCH_RADIUS = 100; const MAX_LENGTH_TOLERANCE = 0.5; const MAX_CANDIDATES = 10; const MAX_BEARING_TOLERANCE = 180; // 360 +/- tolerance const REFERNECE_GEOMETRY_OFFSET = 2; const MAX_ROUTE_QUERIES = 16; // TODO need to pull this from PBF enum defintion // @property {number} Motorway=0 Motorway value // * @property {number} Trunk=1 Trunk value // * @property {number} Primary=2 Primary value // * @property {number} Secondary=3 Secondary value // * @property {number} Tertiary=4 Tertiary value // * @property {number} Residential=5 Residential value // * @property {number} Unclassified=6 Unclassified value // * @property {number} Service=7 Service value // * @property {number} Other=8 Other value function roadClassConverter(roadClass) { if (roadClass === 'Motorway') return 0; else if (roadClass === 'Trunk') return 1; else if (roadClass === 'Primary') return 2; else if (roadClass === 'Secondary') return 3; else if (roadClass === 'Tertiary') return 4; else if (roadClass === 'Residential') return 5; else if (roadClass === 'Unclassified') return 6; else if (roadClass === 'Service') return 7; else return null; } function angleDelta(a1, a2) { var delta = 180 - Math.abs(Math.abs(a1 - a2) - 180); return delta; } function normalizeAngle(a) { if (a < 0) return a + 360; return a; } var ReferenceDirection; (function (ReferenceDirection) { ReferenceDirection["FORWARD"] = "forward"; ReferenceDirection["BACKWARD"] = "backward"; })(ReferenceDirection = exports.ReferenceDirection || (exports.ReferenceDirection = {})); var ReferenceSideOfStreet; (function (ReferenceSideOfStreet) { ReferenceSideOfStreet["RIGHT"] = "right"; ReferenceSideOfStreet["LEFT"] = "left"; ReferenceSideOfStreet["CENTER"] = "center"; ReferenceSideOfStreet["UNKNOWN"] = "unknown"; })(ReferenceSideOfStreet = exports.ReferenceSideOfStreet || (exports.ReferenceSideOfStreet = {})); class PointCandidate { calcScore() { if (!this.score) { // score for snapped points are average of distance to point on line distance and distance to snapped ponit if (this.snappedPoint) this.score = (this.pointOnLine.properties.dist + distance_1.default(this.searchPoint, this.snappedPoint, { units: 'meters' })) / 2; else this.score = this.pointOnLine.properties.dist; } return this.score; } toFeature() { this.calcScore(); var feature = turfHelpers.feature(this.pointOnLine.geometry, { score: this.score, location: this.location, referenceLength: this.referenceLength, geometryId: this.geometryId, referenceId: this.referenceId, direction: this.direction, bearing: this.bearing, sideOfStreet: this.sideOfStreet, interceptAngle: this.interceptAngle }); return feature; } } exports.PointCandidate = PointCandidate; class GraphNode { } class GraphEdge { } class GraphNodeEdgeRelations { } class PathSegment { constructor() { this.section = []; } isIdentical(otherSegment) { if (this.referenceId === otherSegment.referenceId) { if ((otherSegment.section[0] === this.section[0] && otherSegment.section[1] === this.section[1])) return true; } return false; } isIntersecting(otherSegment) { if (this.referenceId === otherSegment.referenceId) { if ((otherSegment.section[0] <= this.section[1] && otherSegment.section[0] >= this.section[0]) || (otherSegment.section[1] <= this.section[1] && otherSegment.section[1] >= this.section[0])) return true; } return false; } toFeature() { var feature = turfHelpers.feature(this.geometry.geometry, { referenceLength: this.referenceLength, geometryId: this.geometryId, referenceId: this.referenceId, direction: this.direction, sideOfStreet: this.sideOfStreet, section: this.section, roadClass: this.roadClass, streetname: this.streetname, fromIntersectionId: this.fromIntersectionId, toIntersectionId: this.toIntersectionId, fromStreetnames: this.fromStreetnames, toStreetnames: this.toStreetnames, }); return feature; } } exports.PathSegment = PathSegment; class PathCandidate { toDebugView() { var debugCollection = turfHelpers.featureCollection([this.originalFeature, this.matchedPath]); return debugCollection; } getOriginalFeatureLength() { if (!this.originalFeatureLength) this.originalFeatureLength = length_1.default(this.originalFeature, { "units": "meters" }); return this.originalFeatureLength; } getPathLength() { if (!this.pathLength) { this.pathLength = 0; for (var segment of this.segments) { if (segment.section) this.pathLength = this.pathLength + (segment.section[1] - segment.section[0]); else this.pathLength = this.pathLength + segment.referenceLength; } } return this.pathLength; } getLengthDelta() { return this.getPathLength() - this.getOriginalFeatureLength(); } isColinear(candidate) { if (this.segments.length > 0 && candidate.segments.length > 0 && this.segments.length == candidate.segments.length) { var path1GeometryIds = new Set(); var path2GeometryIds = new Set(); for (var segment of this.segments) { path1GeometryIds.add(segment.geometryId); } for (var segment of candidate.segments) { path2GeometryIds.add(segment.geometryId); if (!path1GeometryIds.has(segment.geometryId)) return false; } for (var segment of this.segments) { if (!path2GeometryIds.has(segment.geometryId)) return false; } } else return false; return true; } } exports.PathCandidate = PathCandidate; class LevelDB { constructor(directory) { this.db = levelup(leveldown(directory)); } get(key) { return __awaiter(this, void 0, void 0, function* () { try { var data = yield this.db.get(key); return data.toString(); } catch (error) { return null; } }); } put(key, data) { return __awaiter(this, void 0, void 0, function* () { return yield this.db.put(key, data); }); } has(key) { return __awaiter(this, void 0, void 0, function* () { try { yield this.db.get(key); return true; } catch (error) { return null; } }); } } exports.LevelDB = LevelDB; function streamPromise(stream) { return new Promise((resolve, reject) => { stream.on('end', () => { resolve('end'); }); stream.on('error', (error) => { reject(error); }); }); } class Graph { constructor(extent, tileParams, graphMode = GraphMode.CAR_ALL, existingTileIndex = null) { // options this.searchRadius = DEFAULT_SEARCH_RADIUS; this.snapIntersections = false; this.useHMM = true; this.useDirect = true; this.includeStreetnames = true; this.bearingTolerance = DEFAULT_BEARING_TOLERANCE; this.tileParams = tileParams; this.tilePathGroup = tiles_1.TilePathGroup.fromPolygon(extent, 1000, tileParams); this.tilePathGroup.addType(tiles_1.TileType.GEOMETRY); this.tilePathGroup.addType(tiles_1.TileType.REFERENCE); if (this.includeStreetnames) { this.tilePathGroup.addType(tiles_1.TileType.METADATA); this.tilePathGroup.addType(tiles_1.TileType.INTERSECTION); } this.graphMode = graphMode; var paths = []; for (var path of this.tilePathGroup) { paths.push(path.toPathString()); } if (existingTileIndex) { this.tileIndex = existingTileIndex; } else { this.tileIndex = new tile_index_1.TileIndex(); } // create id from tile path hash this.id = uuidHash(this.graphMode + ' node-pair.sv1 ' + paths.join(" ")); } createGraphXml() { return __awaiter(this, void 0, void 0, function* () { // build xml representation of graph + leveldb id map var graphPath = path.join(SHST_GRAPH_CACHE_DIR, this.id); var nextNodeId = 1; var nextEdgeId = 1; var xmlPath = path.join(graphPath, '/graph.xml'); // create xml stream const pipeline = util.promisify(stream.pipeline); var xmlStreamWriter = fs_1.createWriteStream(xmlPath); var osmRootElem = xml.element({ _attr: { version: '0.6', generator: 'shst cli v1.0' } }); var xmlStream = xml({ osm: osmRootElem }, { stream: true }); xmlStream.on('data', function (chunk) { xmlStreamWriter.write(chunk); }); const writeNode = (lon, lat, shstIntersectionId = null) => __awaiter(this, void 0, void 0, function* () { var nodeId; // check if intersection node already written if (shstIntersectionId && (yield this.db.has(shstIntersectionId))) { var node = JSON.parse(yield this.db.get(shstIntersectionId)); nodeId = node.nodeId; } else { nodeId = nextNodeId; var newNode = new GraphNode(); newNode.nodeId = nodeId; if (shstIntersectionId) { newNode.shstIntersectionId = shstIntersectionId; yield this.db.put(shstIntersectionId, JSON.stringify(newNode)); } yield this.db.put('node:' + nodeId, JSON.stringify(newNode)); // write node xml osmRootElem.push({ node: [{ _attr: { id: nodeId, lat: lat, lon: lon } }] }); nextNodeId++; } return nodeId; }); const pushNodePair = (nodePairId, refId) => __awaiter(this, void 0, void 0, function* () { if (yield this.db.has(nodePairId)) { var existingEdges = JSON.parse(yield this.db.get(nodePairId)); existingEdges.push(refId); yield this.db.put(nodePairId, JSON.stringify(existingEdges)); } else { yield this.db.put(nodePairId, JSON.stringify([refId])); } }); for (var obj of this.tileIndex.objectIndex.values()) { if (geomInstance(obj)) { if (obj.roadClass == 'Motorway') { if (this.graphMode != GraphMode.CAR_ALL && this.graphMode != GraphMode.CAR_MOTORWAY_ONLY) { continue; } } else { if (this.graphMode == GraphMode.CAR_MOTORWAY_ONLY) { continue; } } if (obj.roadClass == 'Other') { if (this.graphMode != GraphMode.BIKE && this.graphMode != GraphMode.PEDESTRIAN) { continue; } } // iterate through coordinates and build nodes var coords = index_1.lonlatsToCoords(obj.lonlats); var nodeIds = []; for (var i = 0; i < coords.length; i++) { var shstIntersectionId = null; if (i === 0) shstIntersectionId = obj.fromIntersectionId; else if (i === coords.length - 1) shstIntersectionId = obj.toIntersectionId; var nodeId = yield writeNode(coords[i][0], coords[i][1], shstIntersectionId); nodeIds.push(nodeId); } // var edge:GraphEdge = new GraphEdge(); // edge.shstGeometryId = obj.id; // edge.edgeId = nextEdgeId; // await this.db.put(obj.id, JSON.stringify(edge)); // await this.db.put('edge:' + edge.edgeId, JSON.stringify(edge)); var nodeIdElems = []; var previousNode = null; for (nodeId of nodeIds) { //var nodeEdgeRelation:GraphNodeEdgeRelations; // if(await this.db.has('node-edges:' + nodeId)){ // // insert new edge into existing node-edge relation set for node id // nodeEdgeRelation = JSON.parse(await this.db.get('node-edges:' + nodeId)) // var edgeIds = new Set(nodeEdgeRelation.edgeIds); // edgeIds.add(edge.edgeId); // nodeEdgeRelation.edgeIds = [...edgeIds.values()]; // } // else { // // create node-edge relation set for node id // nodeEdgeRelation = new GraphNodeEdgeRelations(); // nodeEdgeRelation.nodeId = nodeId; // nodeEdgeRelation.edgeIds = [edge.edgeId]; // } // await this.db.put('node-edges:' + nodeId, JSON.stringify(nodeEdgeRelation)); nodeIdElems.push({ nd: [{ _attr: { ref: nodeId } }] }); if (previousNode) { pushNodePair('node-pair:' + nodeId + '-' + previousNode, obj.forwardReferenceId); if (obj.backReferenceId) pushNodePair('node-pair:' + previousNode + '-' + nodeId, obj.backReferenceId); } previousNode = nodeId; } var oneWay = obj.backReferenceId ? 'no' : 'yes'; var roadClass = obj.roadClass.toLocaleLowerCase(); if (roadClass == "other") roadClass = "path"; // TODO add bike/ped modal restrictions to paths osmRootElem.push({ way: [{ _attr: { id: nextEdgeId } }, { tag: { _attr: { k: 'highway', v: roadClass } } }, { tag: { _attr: { k: 'oneway', v: oneWay } } }, ...nodeIdElems] }); nextEdgeId++; } } const writeFinished = new Promise((resolve, reject) => { xmlStreamWriter.on('finish', () => { resolve(xmlPath); }); }); osmRootElem.close(); xmlStreamWriter.close(); //xmlStream.end(); return writeFinished; }); } buildGraph() { return __awaiter(this, void 0, void 0, function* () { // check if graph is already built; if (this.osrm) return; try { var graphPath = path.join(SHST_GRAPH_CACHE_DIR, this.id); var dbPath = path.join(graphPath, '/db'); yield this.tileIndex.indexTilesByPathGroup(this.tilePathGroup); if (USE_LOCAL_CACHE && fs_1.existsSync(dbPath)) { var osrmPath = path.join(graphPath, '/graph.xml.osrm'); console.log(chalk.keyword('lightgreen')(" loading pre-built " + this.graphMode + " graph from: " + osrmPath)); this.db = new LevelDB(dbPath); if (OPTIMIZE_GRAPH) this.osrm = new OSRM({ path: osrmPath }); else this.osrm = new OSRM({ path: osrmPath, algorithm: "MLD" }); } else { if (!OSRM_DIR) { console.log("unable to locate OSRM module."); throw "unable to locate OSRM module."; } // TODO before building, check if this graph is a subset of an existing graph console.log(chalk.keyword('lightgreen')(" building graph using OSRM from: " + OSRM_DIR)); fs_1.mkdirSync(dbPath, { recursive: true }); this.db = new LevelDB(dbPath); console.log(chalk.keyword('lightgreen')(" building " + this.graphMode + " graph xml...")); var xmlPath = yield this.createGraphXml(); //extract console.log(chalk.keyword('lightgreen')(" building " + this.graphMode + " graph from: " + xmlPath)); var profile; if (this.graphMode === GraphMode.CAR_ALL || this.graphMode === GraphMode.CAR_SURFACE_ONLY || this.graphMode === GraphMode.CAR_MOTORWAY_ONLY) profile = path.join(OSRM_DIR, 'profiles/car.lua'); else if (this.graphMode === GraphMode.BIKE) profile = path.join(OSRM_DIR, 'profiles/bicycle.lua'); else if (this.graphMode === GraphMode.PEDESTRIAN) profile = path.join(OSRM_DIR, 'profiles/foot.lua'); child_process_1.execSync(path.join(OSRM_DIR, 'lib/binding/osrm-extract') + ' ' + xmlPath + ' -p ' + profile); var osrmPath = xmlPath + '.osrm'; if (OPTIMIZE_GRAPH) { console.log(chalk.keyword('lightgreen')(" optimizing graph...")); child_process_1.execSync(path.join(OSRM_DIR, 'lib/binding/osrm-contract') + ' ' + osrmPath); this.osrm = new OSRM({ path: osrmPath }); } else { child_process_1.execSync(path.join(OSRM_DIR, 'lib/binding/osrm-partition') + ' ' + osrmPath); child_process_1.execSync(path.join(OSRM_DIR, 'lib/binding/osrm-customize') + ' ' + osrmPath); console.log(chalk.keyword('lightgreen')(" skipping graph optimization...")); this.osrm = new OSRM({ path: osrmPath, algorithm: "MLD" }); } } } catch (e) { console.log("Unable to build graph: " + e); console.log("Try deleting existing cached graph: " + graphPath); } }); } matchTrace(feature) { return __awaiter(this, void 0, void 0, function* () { // fall back to hmm for probabilistic path discovery if (!this.osrm) throw "Graph not buit. call buildGraph() before running queries."; var hmmOptions = { coordinates: feature.geometry.coordinates, annotations: true, geometries: 'geojson', radiuses: Array(feature.geometry.coordinates.length).fill(this.searchRadius) }; try { var matches = yield new Promise((resolve, reject) => { this.osrm.match(hmmOptions, function (err, response) { if (err) reject(err); else resolve(response); }); }); var visitedEdges = new Set(); var visitedEdgeList = []; if (matches['matchings'] && matches['matchings'].length > 0) { var match = matches['matchings'][0]; if (0 < match.confidence) { // this is kind of convoluted due to the sparse info returned in the OSRM annotations... // write out sequence of nodes and edges as emitted from walking OSRM-returned nodes // finding the actual posistion and directionality of the OSRM-edge within the ShSt graph // edge means that we have to snap start/end points in the OSRM geom //console.log(JSON.stringify(match.geometry)); var edgeCandidates; var nodes = []; var visitedNodes = new Set(); // ooof this is brutual -- need to unpack legs and reduce list... for (var leg of match['legs']) { //console.log(leg['annotation']['nodes']) for (var n of leg['annotation']['nodes']) { if (!visitedNodes.has(n) || nodes.length == 0) nodes.push(n); visitedNodes.add(n); } } // then group node pairs into unique edges... var previousNode = null; for (var nodeId of nodes) { if (yield this.db.has('node:' + nodeId)) { if (previousNode) { if (yield this.db.has('node-pair:' + nodeId + '-' + previousNode)) { var edges = JSON.parse(yield this.db.get('node-pair:' + nodeId + '-' + previousNode)); for (var edge of edges) { if (!visitedEdges.has(edge)) visitedEdgeList.push(edge); visitedEdges.add(edge); } } } previousNode = nodeId; } } } } var pathCandidate = new PathCandidate(); pathCandidate.matchType = MatchType.HMM; pathCandidate.confidence = match.confidence; pathCandidate.originalFeature = feature; pathCandidate.segments = []; var segmentGeoms = turfHelpers.multiLineString([]); for (var edgeReferenceId of visitedEdgeList) { var edgeRef = this.tileIndex.objectIndex.get(edgeReferenceId); var edgeGeom = this.tileIndex.objectIndex.get(edgeRef.geometryId); var pathSegment = new PathSegment(); pathSegment.geometryId = edgeGeom.id; pathSegment.referenceId = edgeReferenceId; // TODO calc directionality from graph edge trajectory possible... pathCandidate.segments.push(pathSegment); var segementGeom = this.tileIndex.featureIndex.get(edgeGeom.id); segmentGeoms.geometry.coordinates.push(segementGeom.geometry.coordinates); } if (pathCandidate.segments.length > 0) pathCandidate.matchedPath = segmentGeoms; return pathCandidate; } catch (e) { return null; } }); } matchGeom(feature) { return __awaiter(this, void 0, void 0, function* () { var pathCandidates = []; // fall back to hmm for probabilistic path discovery if (!this.osrm) throw "Graph not buit. call buildGraph() before running queries."; var hmmOptions = { coordinates: feature.geometry.coordinates, annotations: true, geometries: 'geojson', radiuses: Array(feature.geometry.coordinates.length).fill(this.searchRadius) }; try { var matches = yield new Promise((resolve, reject) => { this.osrm.match(hmmOptions, function (err, response) { if (err) reject(err); else resolve(response); }); }); var visitedEdges = new Set(); var visitedEdgeList = []; if (matches['matchings'] && matches['matchings'].length > 0) { var match = matches['matchings'][0]; if (0 < match.confidence) { // this is kind of convoluted due to the sparse info returned in the OSRM annotations... // write out sequence of nodes and edges as emitted from walking OSRM-returned nodes // finding the actual posistion and directionality of the OSRM-edge within the ShSt graph // edge means that we have to snap start/end points in the OSRM geom //console.log(JSON.stringify(match.geometry)); var edgeCandidates; var nodes = []; var visitedNodes = new Set(); // ooof this is brutual -- need to unpack legs and reduce list... for (var leg of match['legs']) { //console.log(leg['annotation']['nodes']) for (var n of leg['annotation']['nodes']) { if (!visitedNodes.has(n) || nodes.length == 0) nodes.push(n); visitedNodes.add(n); } } // then group node pairs into unique edges... var previousNode = null; for (var nodeId of nodes) { if (yield this.db.has('node:' + nodeId)) { if (previousNode) { if (yield this.db.has('node-pair:' + nodeId + '-' + previousNode)) { var edges = JSON.parse(yield this.db.get('node-pair:' + nodeId + '-' + previousNode)); for (var edge of edges) { if (!visitedEdges.has(edge)) visitedEdgeList.push(edge); visitedEdges.add(edge); } } } previousNode = nodeId; } } } } if (visitedEdgeList.length > 0) { var startPoint = turfHelpers.point(feature.geometry.coordinates[0]); var endPoint = turfHelpers.point(feature.geometry.coordinates[feature.geometry.coordinates.length - 1]); var startCandidate = yield this.getPointCandidateFromRefId(startPoint, visitedEdgeList[0], null); var endCandidate = yield this.getPointCandidateFromRefId(endPoint, visitedEdgeList[visitedEdgeList.length - 1], null); var alreadyIncludedPaths = new Set(); var pathCandidate = new PathCandidate(); var matchWorked = true; pathCandidate.matchType = MatchType.HMM; pathCandidate.score = match.confidence; pathCandidate.originalFeature = feature; pathCandidate.matchedPath = turfHelpers.feature(match.geometry); //console.log(JSON.stringify(pathCandidate.matchedPath)); pathCandidate.segments = []; var length = pathCandidate.getOriginalFeatureLength(); for (var k = 0; k < visitedEdgeList.length; k++) { var pathSegment = new PathSegment(); pathSegment.referenceId = visitedEdgeList[k]; var shstRef = this.tileIndex.objectIndex.get(visitedEdgeList[k]); pathSegment.referenceId = visitedEdgeList[k]; pathSegment.geometryId = shstRef.geometryId; pathCandidate.segments.push(pathSegment); pathCandidate.segments[k].referenceLength = getReferenceLength(shstRef); } if (pathCandidate.segments.length > 0) { pathCandidate.startPoint = startCandidate; // build directionality into edge sequences for (var k = 0; k < pathCandidate.segments.length; k++) { var edgeGeom = this.tileIndex.objectIndex.get(pathCandidate.segments[k].geometryId); pathCandidate.segments[k].roadClass = roadClassConverter(edgeGeom.roadClass); // if start and end are on the same graph edge make sure they're the same referenceId/direction if (startCandidate.referenceId == endCandidate.referenceId) { pathCandidate.endPoint = endCandidate; pathCandidate.segments[k].section = [startCandidate.location, endCandidate.location]; } else { if (k == pathCandidate.segments.length - 1) { pathCandidate.endPoint = endCandidate; pathCandidate.segments[k].section = [0, endCandidate.location]; } else if (k == 0) { pathCandidate.segments[k].section = [pathCandidate.startPoint.location, pathCandidate.segments[k].referenceLength]; } else { pathCandidate.segments[k].section = [0, pathCandidate.segments[k].referenceLength]; } } // put to/from on semgnet if (edgeGeom.forwardReferenceId == pathCandidate.segments[k].referenceId) { pathCandidate.segments[k].fromIntersectionId = edgeGeom.fromIntersectionId; pathCandidate.segments[k].toIntersectionId = edgeGeom.toIntersectionId; } else { // reverse to/from for back references pathCandidate.segments[k].fromIntersectionId = edgeGeom.toIntersectionId; pathCandidate.segments[k].toIntersectionId = edgeGeom.fromIntersectionId; } } if (pathCandidate.startPoint.sideOfStreet == pathCandidate.endPoint.sideOfStreet) pathCandidate.sideOfStreet = pathCandidate.startPoint.sideOfStreet; else pathCandidate.sideOfStreet = ReferenceSideOfStreet.UNKNOWN; var startDist = distance_1.default(startPoint, startCandidate.pointOnLine, { "units": "meters" }); var endDist = distance_1.default(endPoint, endCandidate.pointOnLine, { "units": "meters" }); pathCandidate.score = Math.round((util_1.rmse([startDist, endDist, pathCandidate.getLengthDelta()]) * 100)) / 100; pathCandidates.push(pathCandidate); // var refIdHash = uuidHash(pathCandidate.segments.map((value):string => {return value.referenceId; }).join(' ')); // if(matchWorked && !alreadyIncludedPaths.has(refIdHash)) { // alreadyIncludedPaths.add(refIdHash); // bestPathCandidate = pathCandidate; // } } } } catch (e) { // no-op failed to match } if (pathCandidates.length > 0) { var bestPathCandidate = pathCandidates[0]; var cleanedPath = []; var segCoords = []; var totalPathLength = 0; for (var i = 0; i < bestPathCandidate.segments.length; i++) { totalPathLength += bestPathCandidate.segments[i].section[1] - bestPathCandidate.segments[i].section[0]; } for (var i = 0; i < bestPathCandidate.segments.length; i++) { var segment = bestPathCandidate.segments[i]; // adding fudge factor for decimal precision issues if (segment.section[0] < segment.section[1] + this.searchRadius && segment.section[1] <= segment.referenceLength + this.searchRadius && segment.section[0] + this.searchRadius >= 0) { if (this.snapIntersections && (totalPathLength > this.searchRadius)) { if (i == 0 && segment.referenceLength - segment.section[0] < this.searchRadius) continue; if (i == 0 && segment.section[0] < this.searchRadius) segment.section[0] = 0; if (i == bestPathCandidate.segments.length - 1 && segment.section[1] < this.searchRadius) continue; if (i == bestPathCandidate.segments.length - 1 && segment.referenceLength - segment.section[1] < this.searchRadius) segment.section[1] = segment.referenceLength; if (i > 0 && i < bestPathCandidate.segments.length - 1) { segment.section[0] = 0; segment.section[1] = segment.referenceLength; } } if (segment.section[0] == segment.section[1]) continue; segment.geometry = yield this.tileIndex.geom(segment.referenceId, segment.section[0], segment.section[1]); if (segment.geometry) { cleanedPath.push(segment); segCoords.push(segment.geometry.geometry.coordinates); } } } bestPathCandidate.segments = cleanedPath; if (cleanedPath.length == 0) return null; if (segCoords.length > 0) { var segmentGeoms = turfHelpers.multiLineString([]); segmentGeoms.geometry.coordinates = [...segCoords]; bestPathCandidate.matchedPath = segmentGeoms; } } return bestPathCandidate; }); } directionForRefId(refId) { var ref = this.tileIndex.objectIndex.get(refId); if (ref) { var geom = this.tileIndex.objectIndex.get(ref['geometryId']); if (geom) { if (geom['forwardReferenceId'] === ref['id']) return ReferenceDirection.FORWARD; else if (geom['backReferenceId'] === ref['id']) return ReferenceDirection.BACKWARD; } } return null; } toIntersectionIdForRefId(refId) { var ref = this.tileIndex.objectIndex.get(refId); return ref.locationReferences[ref.locationReferences.length - 1].intersectionId; } fromIntersectionIdForRefId(refId) { var ref = this.tileIndex.objectIndex.get(refId); return ref.locationReferences[0].intersectionId; } getPointCandidateFromRefId(searchPoint, refId, searchBearing) { return __awaiter(this, void 0, void 0, function* () { var reference = this.tileIndex.objectIndex.get(refId); var geometry = this.tileIndex.objectIndex.get(reference.geometryId); var geometryFeature = this.tileIndex.featureIndex.get(reference.geometryId); var direction = ReferenceDirection.FORWARD; if (geometry.backReferenceId && geometry.backReferenceId === refId) direction = ReferenceDirection.BACKWARD; var pointOnLine = nearest_point_on_line_1.default(geometryFeature, searchPoint, { units: 'meters' }); if (pointOnLine.properties.dist < this.searchRadius) { var refLength = 0; for (var lr of reference.locationReferences) { if (lr.distanceToNextRef) refLength = refLength + (lr.distanceToNextRef / 100); } var interceptBearing = normalizeAngle(bearing_1.default(pointOnLine, searchPoint)); var i = pointOnLine.properties.index; if (geometryFeature.geometry.coordinates.length <= i + 1) i = i - 1; var lineBearing = bearing_1.default(geometryFeature.geometry.coordinates[i], geometryFeature.geometry.coordinates[i + 1]); if (direction === ReferenceDirection.BACKWARD) lineBearing += 180; lineBearing = normalizeAngle(lineBearing); var pointCandidate = new PointCandidate(); pointCandidate.searchPoint = searchPoint; pointCandidate.pointOnLine = pointOnLine; pointCandidate.geometryId = geometryFeature.properties.id; pointCandidate.referenceId = reference.id; pointCandidate.roadClass = roadClassConverter(geometry.roadClass); // if(this.includeStreetnames) { // var metadata = await this.cache.metadataById(pointCandidate.geometryId); // pointCandidate.streetname = metadata.name; // } pointCandidate.direction = direction; pointCandidate.referenceLength = refLength; if (direction === ReferenceDirection.FORWARD) pointCandidate.location = pointOnLine.properties.location; else pointCandidate.location = refLength - pointOnLine.properties.location; pointCandidate.bearing = normalizeAngle(lineBearing); pointCandidate.interceptAngle = normalizeAngle(interceptBearing - lineBearing); pointCandidate.sideOfStreet = ReferenceSideOfStreet.UNKNOWN; if (pointCandidate.interceptAngle < 180) { pointCandidate.sideOfStreet = ReferenceSideOfStreet.RIGHT; } if (pointCandidate.interceptAngle > 180) { pointCandidate.sideOfStreet = ReferenceSideOfStreet.LEFT; } if (geometry.backReferenceId) pointCandidate.oneway = false; else pointCandidate.oneway = true; // check bearing and add to candidate list if (!searchBearing || angleDelta(searchBearing, lineBearing) < this.bearingTolerance) return pointCandidate; } return null; }); } getPointCandidateFromGeom(searchPoint, pointOnLine, candidateGeom, candidateGeomFeature, searchBearing, direction) { if (pointOnLine.properties.dist < this.searchRadius) { var reference; if (direction === ReferenceDirection.FORWARD) { reference = this.tileIndex.objectIndex.get(candidateGeom.forwardReferenceId); } else { if (candidateGeom.backReferenceId) reference = this.tileIndex.objectIndex.get(candidateGeom.backReferenceId); else return null; // no back-reference } var refLength = 0; for (var lr of reference.locationReferences) { if (lr.distanceToNextRef) refLength = refLength + (lr.distanceToNextRef / 100); } var interceptBearing = normalizeAngle(bearing_1.default(pointOnLine, searchPoint)); var i = pointOnLine.properties.index; if (candidateGeomFeature.geometry.coordinates.length <= i + 1) i = i - 1; var lineBearing = bearing_1.default(candidateGeomFeature.geometry.coordinates[i], candidateGeomFeature.geometry.coordinates[i + 1]); if (direction === ReferenceDirection.BACKWARD) lineBearing += 180; lineBearing = normalizeAngle(lineBearing); var pointCandidate = new PointCandidate(); pointCandidate.searchPoint = searchPoint; pointCandidate.pointOnLine = pointOnLine; pointCandidate.geometryId = candidateGeomFeature.properties.id; pointCandidate.referenceId = reference.id; pointCandidate.roadClass = roadClassConverter(candidateGeom.roadClass); // if(this.includeStreetnames) { // var metadata = await this.cache.metadataById(pointCandidate.geometryId); // pointCandidate.streetname = metadata.name; // } pointCandidate.direction = direction; pointCandidate.referenceLength = refLength; if (direction === ReferenceDirection.FORWARD) pointCandidate.location = pointOnLine.properties.location; else pointCandidate.location = refLength - pointOnLine.properties.location; pointCandidate.bearing = normalizeAngle(lineBearing); pointCandidate.interceptAngle = normalizeAngle(interceptBearing - lineBearing); pointCandidate.sideOfStreet = ReferenceSideOfStreet.UNKNOWN; if (pointCandidate.interceptAngle < 180) { pointCandidate.sideOfStreet = ReferenceSideOfStreet.RIGHT; } if (pointCandidate.interceptAngle > 180) { pointCandidate.sideOfStreet = ReferenceSideOfStreet.LEFT; } if (candidateGeom.backReferenceId) pointCandidate.oneway = false; else pointCandidate.oneway = true; // check bearing and add to candidate list if (!searchBearing || angleDelta(searchBearing, lineBearing) < this.bearingTolerance) return pointCandidate; } return null; } joinPoints(startPoint, endPoint, intersectionBuffer, offsetLine) { return __awaiter(this, void 0, void 0, function* () { let segmentStart = startPoint.location; let segmentEnd = endPoint.location; let segmentLength = segmentEnd - segmentStart; if (intersectionBuffer * 2 < startPoint.referenceLength) { if (segmentStart < intersectionBuffer) { segmentStart = intersectionBuffer; if (segmentStart > segmentEnd) { segmentEnd = segmentStart + segmentLength; if (segmentEnd > startPoint.referenceLength) { segmentEnd = startPoint.referenceLength - intersectionBuffer; } } } if (startPoint.referenceLength - segmentEnd < intersectionBuffer) segmentEnd = startPoint.referenceLength - intersectionBuffer; if (segmentStart > segmentEnd) { segmentStart = segmentEnd - segmentLength; if (segmentStart < 0) segmentStart = intersectionBuffer; } } var joinedPoint = new PathSegment(); joinedPoint.section[0] = segmentStart; joinedPoint.section[1] = segmentEnd; joinedPoint.geometryId = startPoint.geometryId; joinedPoint.referenceId = startPoint.referenceId; joinedPoint.referenceLength = startPoint.referenceLength; joinedPoint.sideOfStreet = startPoint.sideOfStreet; if (joinedPoint.sideOfStreet == ReferenceSideOfStreet.LEFT) offsetLine = 0 - offsetLine; joinedPoint.geometry = (yield this.tileIndex.geom(joinedPoint.referenceId, joinedPoint.section[0], joinedPoint.section[1], offsetLine)); if (!joinedPoint.geometry) { joinedPoint.geometry = (yield this.tileIndex.geom(joinedPoint.referenceId, joinedPoint.section[0], joinedPoint.section[1])); }