sharedstreets
Version:
SharedStreets, a 'digital commons' for the street
487 lines (356 loc) • 15.4 kB
text/typescript
'use strict';
import * as turfHelpers from '@turf/helpers';
import lineSliceAlong from '@turf/line-slice-along';
import along from '@turf/along';
import bearing from '@turf/bearing';
import distance from '@turf/distance';
import length from '@turf/length';
import nearestPointOnLine from '@turf/nearest-point-on-line';
import { rmse } from './util';
import { reverseLineString } from './geom';
import { TileIndex } from './tile_index';
import { TilePathParams, TileType } from './tiles';
import { Feature, LineString } from '@turf/buffer/node_modules/@turf/helpers';
import { RoadClass, SharedStreetsReference } from 'sharedstreets-types';
import { SharedStreetsGeometry } from 'sharedstreets-pbf/proto/sharedstreets';
import { forwardReference, backReference } from './index';
// import { start } from 'repl';
// import { normalize } from 'path';
const DEFAULT_SEARCH_RADIUS = 25;
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:string):number {
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;
}
export enum ReferenceDirection {
FORWARD = "forward",
BACKWARD = "backward"
}
export enum ReferenceSideOfStreet {
RIGHT = "right",
LEFT = "left",
UNKNOWN = "unknown"
}
interface SortableCanddate {
score:number;
calcScore():number;
}
export class PointCandidate implements SortableCanddate {
score:number;
searchPoint:turfHelpers.Feature<turfHelpers.Point>;
pointOnLine:turfHelpers.Feature<turfHelpers.Point>;
snappedPoint:turfHelpers.Feature<turfHelpers.Point>;
geometryId:string;
referenceId:string;
roadClass:RoadClass;
direction:ReferenceDirection;
streetname:string;
referenceLength:number;
location:number;
bearing:number;
interceptAngle:number;
sideOfStreet:ReferenceSideOfStreet;
oneway:boolean;
calcScore():number {
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(this.searchPoint, this.snappedPoint, {units: 'meters'})) / 2;
else
this.score = this.pointOnLine.properties.dist;
}
return this.score;
}
toFeature():turfHelpers.Feature<turfHelpers.Point> {
this.calcScore();
var feature:turfHelpers.Feature<turfHelpers.Point> = 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;
}
}
export class PointMatcher {
tileIndex:TileIndex;
searchRadius:number = DEFAULT_SEARCH_RADIUS;
bearingTolerance:number = DEFAULT_BEARING_TOLERANCE;
lengthTolerance:number = DEFAULT_LENGTH_TOLERANCE;
includeIntersections:boolean = false;
includeStreetnames:boolean = false;
ignoreDirection:boolean = false;
snapToIntersections:boolean = false;
snapTopology:boolean = false;
snapSideOfStreet:ReferenceSideOfStreet = ReferenceSideOfStreet.UNKNOWN;
tileParams:TilePathParams;
constructor(extent:turfHelpers.Feature<turfHelpers.Polygon>=null, params:TilePathParams, existingTileIndex:TileIndex=null) {
this.tileParams = params;
if(existingTileIndex)
this.tileIndex = existingTileIndex;
else
this.tileIndex = new TileIndex();
}
directionForRefId(refId:string):ReferenceDirection {
var ref = <SharedStreetsReference>this.tileIndex.objectIndex.get(refId);
if(ref) {
var geom:SharedStreetsGeometry = <SharedStreetsGeometry>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:string):string {
var ref:SharedStreetsReference = <SharedStreetsReference>this.tileIndex.objectIndex.get(refId);
return ref.locationReferences[ref.locationReferences.length - 1].intersectionId;
}
fromIntersectionIdForRefId(refId:string):string {
var ref:SharedStreetsReference = <SharedStreetsReference>this.tileIndex.objectIndex.get(refId);
return ref.locationReferences[0].intersectionId;
}
async getPointCandidateFromRefId(searchPoint:turfHelpers.Feature<turfHelpers.Point>, refId:string, searchBearing:number):Promise<PointCandidate> {
var reference = <SharedStreetsReference>this.tileIndex.objectIndex.get(refId);
var geometry = <SharedStreetsGeometry>this.tileIndex.objectIndex.get(reference.geometryId);
var geometryFeature = <Feature<LineString>>this.tileIndex.featureIndex.get(reference.geometryId);
var direction = ReferenceDirection.FORWARD;
if(geometry.backReferenceId && geometry.backReferenceId === refId)
direction = ReferenceDirection.BACKWARD;
var pointOnLine = nearestPointOnLine(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(pointOnLine, searchPoint));
var i = pointOnLine.properties.index;
if(geometryFeature.geometry.coordinates.length <= i + 1)
i = i - 1;
var lineBearing = bearing(geometryFeature.geometry.coordinates[i], geometryFeature.geometry.coordinates[i + 1]);
if(direction === ReferenceDirection.BACKWARD)
lineBearing += 180;
lineBearing = normalizeAngle(lineBearing);
var pointCandidate:PointCandidate = new PointCandidate();
pointCandidate.searchPoint = searchPoint;
pointCandidate.pointOnLine = pointOnLine;
pointCandidate.geometryId = geometryFeature.properties.id;
pointCandidate.referenceId = reference.id;
pointCandidate.roadClass = 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:turfHelpers.Feature<turfHelpers.Point>, pointOnLine:turfHelpers.Feature<turfHelpers.Point>, candidateGeom:SharedStreetsGeometry, candidateGeomFeature:Feature<LineString>, searchBearing:number, direction:ReferenceDirection):PointCandidate {
if(pointOnLine.properties.dist < this.searchRadius) {
var reference:SharedStreetsReference;
if(direction === ReferenceDirection.FORWARD) {
reference = <SharedStreetsReference>this.tileIndex.objectIndex.get(candidateGeom.forwardReferenceId);
}
else {
if(candidateGeom.backReferenceId)
reference = <SharedStreetsReference>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(pointOnLine, searchPoint));
var i = pointOnLine.properties.index;
if(candidateGeomFeature.geometry.coordinates.length <= i + 1)
i = i - 1;
var lineBearing = bearing(candidateGeomFeature.geometry.coordinates[i], candidateGeomFeature.geometry.coordinates[i + 1]);
if(direction === ReferenceDirection.BACKWARD)
lineBearing += 180;
lineBearing = normalizeAngle(lineBearing);
var pointCandidate:PointCandidate = new PointCandidate();
pointCandidate.searchPoint = searchPoint;
pointCandidate.pointOnLine = pointOnLine;
pointCandidate.geometryId = candidateGeomFeature.properties.id;
pointCandidate.referenceId = reference.id;
pointCandidate.roadClass = 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;
}
async getPointCandidates(searchPoint:turfHelpers.Feature<turfHelpers.Point>, searchBearing:number, maxCandidates:number):Promise<PointCandidate[]> {
this.tileIndex.addTileType(TileType.REFERENCE);
var candidateFeatures = await this.tileIndex.nearby(searchPoint, TileType.GEOMETRY, this.searchRadius, this.tileParams);
var candidates:PointCandidate[] = new Array();
if(candidateFeatures && candidateFeatures.features) {
for(var candidateFeature of candidateFeatures.features) {
var candidateGeom:SharedStreetsGeometry = <SharedStreetsGeometry>this.tileIndex.objectIndex.get(candidateFeature.properties.id);
var candidateGeomFeature:turfHelpers.Feature<turfHelpers.LineString> = <turfHelpers.Feature<turfHelpers.LineString>>this.tileIndex.featureIndex.get(candidateFeature.properties.id);
var pointOnLine = nearestPointOnLine(candidateGeomFeature, searchPoint, {units:'meters'});
var forwardCandidate = await this.getPointCandidateFromGeom(searchPoint, pointOnLine, candidateGeom, candidateGeomFeature, searchBearing, ReferenceDirection.FORWARD);
var backwardCandidate = await this.getPointCandidateFromGeom(searchPoint, pointOnLine, candidateGeom, candidateGeomFeature, searchBearing, ReferenceDirection.BACKWARD);
if(forwardCandidate != null) {
var snapped = false;
if(this.snapToIntersections) {
if(forwardCandidate.location < this.searchRadius) {
var snappedForwardCandidate1 = Object.assign(new PointCandidate, forwardCandidate);
snappedForwardCandidate1.location = 0;
snappedForwardCandidate1.snappedPoint = along(candidateGeomFeature, 0, {"units":"meters"});
candidates.push(snappedForwardCandidate1);
snapped = true;
}
if(forwardCandidate.referenceLength - forwardCandidate.location < this.searchRadius) {
var snappedForwardCandidate2 = Object.assign(new PointCandidate, forwardCandidate);
snappedForwardCandidate2.location = snappedForwardCandidate2.referenceLength;
snappedForwardCandidate2.snappedPoint = along(candidateGeomFeature, snappedForwardCandidate2.referenceLength, {"units":"meters"});
candidates.push(snappedForwardCandidate2);
snapped = true;
}
}
if(!snapped) {
candidates.push(forwardCandidate);
}
}
if(backwardCandidate != null) {
var snapped = false;
if(this.snapToIntersections) {
if(backwardCandidate.location < this.searchRadius) {
var snappedBackwardCandidate1:PointCandidate = Object.assign(new PointCandidate, backwardCandidate);
snappedBackwardCandidate1.location = 0;
// not reversing the geom so snap to end on backRefs
snappedBackwardCandidate1.snappedPoint = along(candidateGeomFeature, snappedBackwardCandidate1.referenceLength, {"units":"meters"});
candidates.push(snappedBackwardCandidate1);
snapped = true;
}
if(backwardCandidate.referenceLength - backwardCandidate.location < this.searchRadius) {
var snappedBackwardCandidate2 = Object.assign(new PointCandidate, backwardCandidate);
snappedBackwardCandidate2.location = snappedBackwardCandidate2.referenceLength;
// not reversing the geom so snap to start on backRefs
snappedBackwardCandidate2.snappedPoint = along(candidateGeomFeature, 0, {"units":"meters"});
candidates.push(snappedBackwardCandidate2);
snapped = true;
}
}
if(!snapped) {
candidates.push(backwardCandidate);
}
}
}
}
var sortedCandidates = candidates.sort((p1, p2) => {
p1.calcScore();
p2.calcScore();
if(p1.score > p2.score) {
return 1;
}
if(p1.score < p2.score) {
return -1;
}
return 0;
});
if(sortedCandidates.length > maxCandidates) {
sortedCandidates = sortedCandidates.slice(0, maxCandidates);
}
return sortedCandidates;
}
}