sharedstreets
Version:
SharedStreets, a 'digital commons' for the street
969 lines (761 loc) • 43.7 kB
text/typescript
import {Command, flags} from '@oclif/command'
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { TilePathParams, TileType, TilePathGroup } from '../index'
import { TileIndex } from '../index'
import { Graph, PathCandidate, GraphMode, ReferenceSideOfStreet} from '../index';
import * as turfHelpers from '@turf/helpers';
import { CleanedLines, reverseLineString, CleanedPoints } from '../geom';
import envelope from '@turf/envelope';
import { forwardReference, backReference } from '../index';
import { PathSegment, PointCandidate } from '../graph';
import { PathSearch } from '../routing';
import { SharedStreetsReference, SharedStreetsIntersection } from 'sharedstreets-types';
import lineOffset from '@turf/line-offset';
import { getReferenceLength } from '../tile_index';
import { generateBinId, getBinCountFromLength, getBinPositionFromLocation, getBinLength } from '../data';
const chalk = require('chalk');
const cliProgress = require('cli-progress');
function mapOgProperties(og_props:{}, new_props:{}) {
for(var prop of Object.keys(og_props)) {
new_props['pp_' + prop] = og_props[prop];
//console.log(new_props)
}
}
export default class Match extends Command {
static description = 'matches point and line features to sharedstreets refs'
static examples = [
`$ shst match points.geojson --out=matched_points.geojson --port-properties
🌏 Loading points...
✨ Matching 3 points...
🎉 Matched 2 points... (1 unmached)
`,
]
static flags = {
help: flags.help({char: 'h'}),
// flag with a value (-o, --out=FILE)
out: flags.string({char: 'o', description: 'file output name creates files [file-output-name].matched.geojson and [file-output-name].unmatched.geojson'}),
'tile-source': flags.string({description: 'SharedStreets tile source', default: 'osm/planet-181224'}),
'tile-hierarchy': flags.integer({description: 'SharedStreets tile hierarchy', default: 6}),
'skip-port-properties': flags.boolean({char: 'p', description: 'skip porting existing feature properties preceeded by "pp_"', default: false}),
'follow-line-direction': flags.boolean({description: 'only match using line direction', default: false}),
'best-direction': flags.boolean({description: 'only match one direction based on best score', default: false}),
'direction-field': flags.string({description: 'name of optional line properity describing segment directionality, use the related "one-way-*-value" and "two-way-value" properties'}),
'one-way-with-direction-value': flags.string({description: 'name of optional value of "direction-field" indicating a one-way street with line direction'}),
'one-way-against-direction-value': flags.string({description: 'name of optional value of "direction-field" indicating a one-way street against line direction'}),
'two-way-value': flags.string({description: 'name of optional value of "direction-field" indicating a two-way street'}),
'bearing-field': flags.string({description: 'name of optional point property containing bearing in decimal degrees', default:'bearing'}),
'search-radius': flags.integer({description: 'search radius for for snapping points, lines and traces (in meters)', default:10}),
'snap-intersections': flags.boolean({description: 'snap line end-points to nearest intersection if closer than distance defined by snap-intersections-radius ', default:false}),
'snap-intersections-radius': flags.integer({description: 'snap radius for intersections (in meters) used when snap-intersections is set', default:10}),
'snap-side-of-street': flags.boolean({description: 'snap line to side of street', default:false}),
'side-of-street-field': flags.string({description: 'name of optional property defining side of street relative to direction of travel'}),
'right-side-of-street-value': flags.string({description: 'value of "side-of-street-field" for right side features', default:'right'}),
'left-side-of-street-value': flags.string({description: 'value of "side-of-street-field" for left side features', default:'left'}),
'center-of-street-value': flags.string({description: 'value of "side-of-street-field" for center features', default:'center'}),
'left-side-driving': flags.boolean({description: 'snap line to side of street using left-side driving rules', default:false}),
'match-car': flags.boolean({description: 'match using car routing rules', default:true}),
'match-bike': flags.boolean({description: 'match using bike routing rules', default:false}),
'match-pedestrian': flags.boolean({description: 'match using pedestrian routing rules', default:false}),
'match-motorway-only': flags.boolean({description: 'only match against motorway segments', default:false}),
'match-surface-streets-only': flags.boolean({description: 'only match against surface street segments', default:false}),
'offset-line': flags.integer({description: 'offset geometry based on direction of matched line (in meters)'}),
'cluster-points': flags.integer({description: 'aproximate sub-segment length for clustering points (in meters)'}),
'buffer-points': flags.boolean({description: 'buffer points into segment-snapped line segments'}),
'buffer-points-length': flags.integer({description: 'length of buffered point (in meters)', default:5}),
'buffer-points-length-field': flags.string({description: 'name of property containing buffered points (in meters)', default:'length'}),
'buffer-merge': flags.boolean({description: 'merge buffered points -- requires related buffer-merge-match-fields to be defined', default:false}),
'buffer-merge-match-fields': flags.string({description: 'comma seperated list of fields to match values when merging buffered points', default:''}),
'buffer-merge-group-fields': flags.string({description: 'comma seperated list of fields to group values when merging buffered points', default:''}),
'join-points': flags.boolean({description: 'joins points into segment-snapped line segments -- requires related join-points-match-fields to be defined'}),
'join-points-match-fields': flags.string({description: 'comma seperated list of fields to match values when joining points', default:''}),
'join-point-sequence-field': flags.string({description: 'name of field containing point sequence (e.g. 1=start, 2=middle, 3=terminus)', default:'point_sequence'}),
'trim-intersections-radius': flags.integer({description: 'buffer and clip radius for intersections in point buffer and point join operations (in meters)', default:0})
}
static args = [{name: 'file'}]
async run() {
const {args, flags} = this.parse(Match)
this.log(chalk.bold.keyword('green')(' 🌏 Loading geojson data...'));
var inFile = args.file;
var outFile = flags.out;
if(!inFile || !existsSync(inFile)) {
this.log(chalk.bold.keyword('orange')(' 💾 Input file not found...'));
return;
}
if(!outFile)
outFile = inFile;
if(outFile.toLocaleLowerCase().endsWith(".geojson"))
outFile = outFile.split(".").slice(0, -1).join(".");
if(flags['direction-field'])
console.log(chalk.bold.keyword('green')(' Filtering one-way and two-way streets using field "' + flags['direction-field'] + '" with values: ' + ' "' + flags['one-way-with-direction-value'] + '", "' + flags['one-way-against-direction-value'] + '", "' + flags['two-way-value'] + '"'));
if(flags['match-bike'] || flags['match-pedestrian']) {
if(flags['match-bike']) {
console.log(chalk.bold.keyword('green')(' Matching using bike routing rules'));
}
if(flags['match-pedestrian']) {
console.log(chalk.bold.keyword('green')(' Matching using pedestrian routing rules'));
}
if(flags['match-motorway-only'])
console.log(chalk.bold.keyword('orange')(' Ignoring motorway-only setting'));
}
else if(flags['match-car']) {
if(flags['match-motorway-only'])
console.log(chalk.bold.keyword('green')(' Matching using car routing rules on motorways only'));
else if(flags['match-surface-only'])
console.log(chalk.bold.keyword('green')(' Matching using car routing rules on surface streets only'));
else
console.log(chalk.bold.keyword('green')(' Matching using car routing rules on all streets'));
}
var content = readFileSync(inFile);
var data:turfHelpers.FeatureCollection<turfHelpers.Geometry> = JSON.parse(content.toLocaleString());
var params = new TilePathParams();
params.source = flags['tile-source'];
params.tileHierarchy = flags['tile-hierarchy']
if(data.features[0].geometry.type === 'LineString' || data.features[0].geometry.type === 'MultiLineString') {
await matchLines(outFile, params, data, flags);
}
else if(data.features[0].geometry.type === 'Point') {
await matchPoints(outFile, params, data, flags);
}
}
}
async function matchPoints(outFile, params, points, flags) {
console.log(chalk.bold.keyword('green')(' ✨ Matching ' + points.features.length + ' points...'));
// test matcher point candidates
var cleanPoints = new CleanedPoints(points)
var graph:Graph = new Graph(null, params);
if(flags['snap-intersections'])
graph.tileIndex.addTileType(TileType.INTERSECTION);
graph.searchRadius = flags['search-radius'];
var unmatchedPoints:turfHelpers.Feature<turfHelpers.Point>[] = [];
const bar2 = new cliProgress.Bar({},{
format: chalk.keyword('blue')(' {bar}') + ' {percentage}% | {value}/{total} ',
barCompleteChar: '\u2588',
barIncompleteChar: '\u2591'
});
class MatchedPointType {originalFeature:turfHelpers.Feature<turfHelpers.Point>; matchedPoint:PointCandidate; bufferedPoint:PathSegment};
var matchedPoints:MatchedPointType[] = [];
bar2.start(points.features.length, 0);
for(var searchPoint of cleanPoints.clean) {
var bearing:number =null;
if(searchPoint.properties && searchPoint.properties[flags['bearing-field']])
bearing = parseFloat(searchPoint.properties[flags['bearing-field']]);
var matches:PointCandidate[] = await graph.matchPoint(searchPoint, bearing, 3, flags['left-side-driving']);
if(matches.length > 0) {
var matchedPoint:MatchedPointType = new MatchedPointType();
matchedPoint.matchedPoint = matches[0];
matchedPoint.originalFeature = searchPoint;
matchedPoints.push(matchedPoint);
}
else {
unmatchedPoints.push(searchPoint);
}
bar2.increment();
}
bar2.stop();
var clusteredPoints = [];
var bufferedPoints = [];
var joinedPoints = [];
var bufferedMergedPoints = [];
var intersectionClusteredPoints = [];
var mergedPoints = [];
if(flags['cluster-points']) {
var clusteredPointMap = {};
var intersectionClusteredPointMap = {};
const mergePointIntoCluster = (matchedPoint:MatchedPointType) => {
var pointGeom = null;
if(flags['snap-intersections'] &&
( matchedPoint.matchedPoint.location <= flags['snap-intersections-radius'] ||
matchedPoint.matchedPoint.referenceLength - matchedPoint.matchedPoint.location <= flags['snap-intersections-radius'])) {
var reference = <SharedStreetsReference>graph.tileIndex.objectIndex.get(matchedPoint.matchedPoint.referenceId);
var intersectionId;
if(matchedPoint.matchedPoint.location <= flags['snap-intersections-radius']) {
intersectionId = reference.locationReferences[0].intersectionId;
}
else if(matchedPoint.matchedPoint.referenceLength - matchedPoint.matchedPoint.location <= flags['snap-intersections-radius']) {
intersectionId = reference.locationReferences[reference.locationReferences.length-1].intersectionId;
}
if(intersectionClusteredPointMap[intersectionId]) {
pointGeom = intersectionClusteredPointMap[intersectionId];
pointGeom.properties['count'] += 1;
}
else {
pointGeom = JSON.parse(JSON.stringify(graph.tileIndex.featureIndex.get(intersectionId)));
var intersection = <SharedStreetsIntersection>graph.tileIndex.objectIndex.get(intersectionId);
delete pointGeom.properties["id"];
pointGeom.properties["intersectionId"] = intersectionId;
var inboundCount = 1;
for(var inboundRefId of intersection.inboundReferenceIds) {
pointGeom.properties["inboundReferenceId_" + inboundCount] = inboundRefId;
inboundCount++;
}
var outboundCount = 1;
for(var outboundRefId of intersection.outboundReferenceIds) {
pointGeom.properties["outboundReferenceId_" + outboundCount] = outboundRefId;
outboundCount++;
}
pointGeom.properties['count'] = 1;
intersectionClusteredPointMap[intersectionId] = pointGeom;
}
}
else {
var binCount = getBinCountFromLength(matchedPoint.matchedPoint.referenceLength, flags['cluster-points'])
var binPosition = getBinPositionFromLocation(matchedPoint.matchedPoint.referenceLength, flags['cluster-points'], matchedPoint.matchedPoint.location);
var binId = generateBinId(matchedPoint.matchedPoint.referenceId, binCount, binPosition);
var binLength = getBinLength(matchedPoint.matchedPoint.referenceLength, flags['cluster-points'])
if(clusteredPointMap[binId]) {
clusteredPointMap[binId].properties['count'] += 1;
}
else {
var bins = graph.tileIndex.referenceToBins(matchedPoint.matchedPoint.referenceId, binCount, 2, ReferenceSideOfStreet.RIGHT);
var binPoint = turfHelpers.point(bins.geometry.coordinates[binPosition - 0]);
binPoint.properties['id'] = binId;
binPoint.properties['referenceId'] = matchedPoint.matchedPoint.referenceId;
binPoint.properties['binPosition'] = binPosition;
binPoint.properties['binCount'] = binCount;
binPoint.properties['binLength'] = binLength;
binPoint.properties['count'] = 1;
clusteredPointMap[binId] = binPoint;
}
pointGeom = clusteredPointMap[binId];
}
for(var property of Object.keys(matchedPoint.originalFeature.properties)) {
if(property.startsWith('pp_')) {
if(!isNaN(matchedPoint.originalFeature.properties[property])) {
var sumPropertyName = 'sum_' + property;
if(!pointGeom.properties[sumPropertyName]) {
pointGeom.properties[sumPropertyName] = 0;
}
pointGeom.properties[sumPropertyName] += matchedPoint.originalFeature.properties[property];
}
}
}
};
for(var matchedPoint of matchedPoints) {
mergePointIntoCluster(matchedPoint);
}
intersectionClusteredPoints = Object.keys(intersectionClusteredPointMap).map(key => intersectionClusteredPointMap[key]);
clusteredPoints = Object.keys(clusteredPointMap).map(key => clusteredPointMap[key]);
}
var offsetLine:number = flags['offset-line'];
if(flags['buffer-points']) {
class MergeBufferedPointsType {mergedPathSegments:PathSegment; matchedPoints:MatchedPointType[]};
console.log(chalk.bold.keyword('green')(' ✨ Buffering ' + matchedPoints.length + ' matched points...'));
var bufferLength = flags['buffer-points-length'];
console.log(chalk.bold.keyword('green')(' default buffer length: ' + bufferLength));
var bufferLengthFieldName = null;
if(flags['buffer-points-length-field']) {
bufferLengthFieldName = flags['buffer-points-length-field'].toLocaleLowerCase().trim().replace(/ /g, "_");
console.log(chalk.bold.keyword('green')(' buffer length fieldname: ' + bufferLengthFieldName));
}
bufferLengthFieldName = flags['buffer-points-length-field'].toLocaleLowerCase().trim().replace(/ /g, "_");
for(var matchedPoint of matchedPoints) {
var leftSideDriving:boolean = flags['left-side-driving'];
if(offsetLine) {
if(leftSideDriving) {
if(matchedPoint.matchedPoint.sideOfStreet === ReferenceSideOfStreet.LEFT) {
offsetLine = offsetLine;
}
else if(matchedPoint.matchedPoint.sideOfStreet === ReferenceSideOfStreet.RIGHT) {
offsetLine = 0 - offsetLine;
}
}
else {
if(matchedPoint.matchedPoint.sideOfStreet === ReferenceSideOfStreet.RIGHT) {
offsetLine = offsetLine;
}
else if(matchedPoint.matchedPoint.sideOfStreet === ReferenceSideOfStreet.LEFT) {
offsetLine = 0 - offsetLine;
}
}
}
var pointBufferLength = bufferLength;
if(bufferLengthFieldName && matchedPoint.originalFeature.properties.hasOwnProperty(bufferLengthFieldName))
pointBufferLength = matchedPoint.originalFeature.properties[bufferLengthFieldName];
matchedPoint.bufferedPoint = await graph.bufferPoint(matchedPoint.matchedPoint, pointBufferLength, offsetLine);
var bufferedFeature = matchedPoint.bufferedPoint.toFeature();
mapOgProperties(matchedPoint.originalFeature.properties, bufferedFeature.properties);
bufferedPoints.push(bufferedFeature);
}
if(flags['buffer-merge']) {
const bufferIntersectionRaidus:number = flags['trim-intersections-radius'];
console.log(chalk.bold.keyword('green')(' ✨ Merging ' + bufferedPoints.length + ' buffered points...'));
let bufferedPreMergedPoints:Map<string,Array<MatchedPointType>> = new Map();
let mergeFields:string[] = [];
if(flags['buffer-merge-match-fields']) {
// split and clean property fields
mergeFields = flags['buffer-merge-match-fields'].split(",").map((f) =>{return f.toLocaleLowerCase().replace(/ /g, "_")});
mergeFields.sort();
console.log(chalk.bold.keyword('green')(' merging on fields: ' + mergeFields.join(', ')));
}
let groupFields:string[] = [];
if(flags['buffer-merge-group-fields']) {
// split and clean property fields
groupFields = flags['buffer-merge-group-fields'].split(",").map((f) =>{return f.toLocaleLowerCase().replace(/ /g, "_")});
groupFields.sort();
console.log(chalk.bold.keyword('green')(' grouping on field values: ' + groupFields.join(', ')));
}
for(let matchedPoint of matchedPoints) {
let fieldValues:string[] = [];
for(let mergeField of mergeFields) {
if(matchedPoint.originalFeature.properties.hasOwnProperty(mergeField)){
fieldValues.push((mergeField + ':' + matchedPoint.originalFeature.properties[mergeField]).toLocaleLowerCase().trim().replace(/ /g, "_"))
}
}
let fieldValuesString = fieldValues.join(':');
let refSideHash = matchedPoint.bufferedPoint.referenceId + ':' + matchedPoint.bufferedPoint.sideOfStreet + ':' + fieldValuesString;
if(!bufferedPreMergedPoints.has(refSideHash)) {
bufferedPreMergedPoints.set(refSideHash, new Array());
};
bufferedPreMergedPoints.get(refSideHash).push(matchedPoint);
}
let mergeSegments = async (bufferedSegments:MatchedPointType[]):Promise<MergeBufferedPointsType[]> => {
let mergedSegment = new MergeBufferedPointsType();
let mergedSegments:MergeBufferedPointsType[] = [];
bufferedSegments
bufferedSegments.sort((a:MatchedPointType, b:MatchedPointType):number => (a.bufferedPoint.section[0] > b.bufferedPoint.section[0]) ? 11 : -1);
let segment1:MatchedPointType = bufferedSegments.pop();
mergedSegment.mergedPathSegments = segment1.bufferedPoint;
mergedSegment.matchedPoints = [segment1];
while(segment1 && bufferedSegments.length > 0) {
let segment2:MatchedPointType = bufferedSegments.pop();
if(segment2 && mergedSegment.mergedPathSegments.isIntersecting(segment2.bufferedPoint)) {
let offsetLine:number = flags['offset-line'];
let leftSideDriving:boolean = flags['left-side-driving'];
if(offsetLine) {
if(leftSideDriving) {
if(mergedSegment.mergedPathSegments.sideOfStreet === ReferenceSideOfStreet.LEFT) {
offsetLine = offsetLine;
}
else if( mergedSegment.mergedPathSegments.sideOfStreet === ReferenceSideOfStreet.RIGHT) {
offsetLine = 0 - offsetLine;
}
}
else {
if(mergedSegment.mergedPathSegments.sideOfStreet === ReferenceSideOfStreet.RIGHT) {
offsetLine = offsetLine;
}
else if(mergedSegment.mergedPathSegments.sideOfStreet === ReferenceSideOfStreet.LEFT) {
offsetLine = 0 - offsetLine;
}
}
}
mergedSegment.mergedPathSegments = await graph.union(mergedSegment.mergedPathSegments, segment2.bufferedPoint, bufferIntersectionRaidus, offsetLine);
mergedSegment.matchedPoints.push(segment2);
}
else {
mergedSegments.push(mergedSegment);
if(segment2) {
segment1 = segment2;
mergedSegment = new MergeBufferedPointsType()
mergedSegment.mergedPathSegments = segment1.bufferedPoint;
mergedSegment.matchedPoints = [segment1];
}
else {
mergedSegment = null;
}
}
}
if(mergedSegment)
mergedSegments.push(mergedSegment);
return mergedSegments;
};
for(let refSide of bufferedPreMergedPoints.keys()) {
if(bufferedPreMergedPoints.get(refSide).length > 0) {
let mergedBuffers:MergeBufferedPointsType[] = await mergeSegments(bufferedPreMergedPoints.get(refSide));
for(let mergedBuffer of mergedBuffers) {
let outputBufferedFeature = mergedBuffer.mergedPathSegments.toFeature();
for(let mergeField of mergeFields) {
if(mergedBuffer.matchedPoints[0].originalFeature.properties.hasOwnProperty(mergeField)){
outputBufferedFeature.properties['pp_' + mergeField] = mergedBuffer.matchedPoints[0].originalFeature.properties[mergeField];
}
}
for(let groupField of groupFields) {
let groupedFieldValues = []
for( let point of mergedBuffer.matchedPoints) {
if(point.originalFeature.properties.hasOwnProperty(groupField)){
groupedFieldValues.push(point.originalFeature.properties[groupField]);
}
}
outputBufferedFeature.properties['pp_' + groupField] = groupedFieldValues;
}
outputBufferedFeature.properties['shst_merged_point_count'] = mergedBuffer.matchedPoints.length;
let mergedBufferLength = 0;
for( let point of mergedBuffer.matchedPoints) {
mergedBufferLength += point.bufferedPoint.section[1] - point.bufferedPoint.section[0];
}
outputBufferedFeature.properties['shst_merged_buffer_length'] = mergedBufferLength;
bufferedMergedPoints.push(outputBufferedFeature);
}
}
}
}
}
if(flags['join-points']) {
class JoinedPointsType {joinedPath:PathSegment; matchedPoints:MatchedPointType[]};
console.log(chalk.bold.keyword('green')(' ✨ Joining ' + matchedPoints.length + ' matched points...'));
var preMergedPoints:Map<string,Array<MatchedPointType>> = new Map();
var mergeFields:string[] = [];
if(flags['join-points-match-fields']) {
// split and clean property fields
mergeFields = flags['join-points-match-fields'].split(",").map((f) =>{return f.toLocaleLowerCase().replace(/ /g, "_")});
mergeFields.sort();
console.log(chalk.bold.keyword('green')(' merging on fields: ' + mergeFields.join(', ')));
}
for(var matchedPoint of matchedPoints) {
let fieldValues:string[] = [];
for(let mergeField of mergeFields) {
if(matchedPoint.originalFeature.properties.hasOwnProperty(mergeField)){
fieldValues.push((mergeField + ':' + matchedPoint.originalFeature.properties[mergeField]).toLocaleLowerCase().trim().replace(/ /g, "_"))
}
}
let fieldValuesString = fieldValues.join(':');
let refSideHash = matchedPoint.matchedPoint.referenceId + ':' + matchedPoint.matchedPoint.sideOfStreet + ':' + fieldValuesString;
if(!preMergedPoints.has(refSideHash)) {
preMergedPoints.set(refSideHash, new Array());
};
preMergedPoints.get(refSideHash).push(matchedPoint);
}
const bufferIntersectionRaidus:number = flags['trim-intersections-radius'];
const mergePoints = async (matchedPoints:MatchedPointType[]):Promise<JoinedPointsType[]> => {
// sort matched points along line
matchedPoints.sort((a, b) => {
return a.matchedPoint.location - b.matchedPoint.location;
});
let joinedSegments:JoinedPointsType[] = [];
let currSegment:JoinedPointsType = null;
for(let matchedPoint of matchedPoints) {
if(!currSegment) {
currSegment = new JoinedPointsType();
currSegment.matchedPoints = [];
if(parseInt(matchedPoint.originalFeature.properties[flags['join-point-sequence-field']]) === 1) {
currSegment.matchedPoints.push(matchedPoint)
}
else if(parseInt(matchedPoint.originalFeature.properties[flags['join-point-sequence-field']]) > 1) {
let startPoint:MatchedPointType = JSON.parse(JSON.stringify(matchedPoint));
startPoint.matchedPoint.location = 0;
currSegment.matchedPoints.push(startPoint)
currSegment.matchedPoints.push(matchedPoint);
if(parseInt(matchedPoint.originalFeature.properties[flags['join-point-sequence-field']]) === 3){
currSegment.joinedPath = await graph.joinPoints(currSegment.matchedPoints[0].matchedPoint, currSegment.matchedPoints[currSegment.matchedPoints.length - 1].matchedPoint, bufferIntersectionRaidus, offsetLine);
joinedSegments.push(currSegment);
currSegment = null;
}
}
}
else if(parseInt(matchedPoint.originalFeature.properties[flags['join-point-sequence-field']]) > 1) {
currSegment.matchedPoints.push(matchedPoint);
if(parseInt(matchedPoint.originalFeature.properties[flags['join-point-sequence-field']]) === 3){
currSegment.joinedPath = await graph.joinPoints(currSegment.matchedPoints[0].matchedPoint, currSegment.matchedPoints[currSegment.matchedPoints.length - 1].matchedPoint, bufferIntersectionRaidus, offsetLine);
joinedSegments.push(currSegment);
currSegment = null;
}
}
}
if(currSegment && currSegment.matchedPoints.length > 0) {
let endPoint:MatchedPointType = JSON.parse(JSON.stringify(currSegment.matchedPoints[currSegment.matchedPoints.length - 1]));
endPoint.matchedPoint.location = endPoint.matchedPoint.referenceLength;
currSegment.matchedPoints.push(endPoint)
currSegment.joinedPath = await graph.joinPoints(currSegment.matchedPoints[0].matchedPoint, currSegment.matchedPoints[currSegment.matchedPoints.length - 1].matchedPoint, bufferIntersectionRaidus, offsetLine);
joinedSegments.push(currSegment);
currSegment = null;
}
return joinedSegments;
};
for(let refSide of preMergedPoints.keys()) {
if(preMergedPoints.get(refSide).length > 0) {
let mergedPointSegments:JoinedPointsType[] = await mergePoints(preMergedPoints.get(refSide));
for(let mergedPointSegment of mergedPointSegments) {
let outputJoinedFeature = mergedPointSegment.joinedPath.toFeature();
for(let mergeField of mergeFields) {
if(mergedPointSegment.matchedPoints[0].originalFeature.properties.hasOwnProperty(mergeField)){
outputJoinedFeature.properties['pp_' + mergeField] = mergedPointSegment.matchedPoints[0].originalFeature.properties[mergeField];
}
}
outputJoinedFeature.properties['shst_joined_point_count'] = mergedPointSegment.matchedPoints.length;
joinedPoints.push(outputJoinedFeature);
}
}
}
}
if(matchedPoints.length) {
console.log(chalk.bold.keyword('blue')(' ✏️ Writing ' + matchedPoints.length + ' matched points: ' + outFile + ".matched.geojson"));
var featureArray = []
for(var matchedPoint of matchedPoints) {
featureArray.push(matchedPoint.matchedPoint.toFeature());
}
var matchedFeatureCollection:turfHelpers.FeatureCollection<turfHelpers.Point> = turfHelpers.featureCollection(featureArray);
var matchedJsonOut = JSON.stringify(matchedFeatureCollection);
writeFileSync(outFile + ".matched.geojson", matchedJsonOut);
}
if(clusteredPoints.length) {
console.log(chalk.bold.keyword('blue')(' ✏️ Writing ' + clusteredPoints.length + ' clustered points: ' + outFile + ".clustered.geojson"));
var clusteredPointsFeatureCollection:turfHelpers.FeatureCollection<turfHelpers.Point> = turfHelpers.featureCollection(clusteredPoints);
var clusteredJsonOut = JSON.stringify(clusteredPointsFeatureCollection);
writeFileSync(outFile + ".clustered.geojson", clusteredJsonOut);
}
if(bufferedPoints.length) {
console.log(chalk.bold.keyword('blue')(' ✏️ Writing ' + bufferedPoints.length + ' buffered points: ' + outFile + ".buffered.geojson"));
var bufferedPointsFeatureCollection:turfHelpers.FeatureCollection<turfHelpers.LineString> = turfHelpers.featureCollection(bufferedPoints);
var bufferedJsonOut = JSON.stringify(bufferedPointsFeatureCollection);
writeFileSync(outFile + ".buffered.geojson", bufferedJsonOut);
}
if(bufferedMergedPoints.length) {
console.log(chalk.bold.keyword('blue')(' ✏️ Writing ' + bufferedMergedPoints.length + ' buffered and merged points: ' + outFile + ".buffered.merged.geojson"));
var bufferedMergedPointsFeatureCollection:turfHelpers.FeatureCollection<turfHelpers.LineString> = turfHelpers.featureCollection(bufferedMergedPoints);
var bufferedMergedJsonOut = JSON.stringify(bufferedMergedPointsFeatureCollection);
writeFileSync(outFile + ".buffered.merged.geojson", bufferedMergedJsonOut);
}
if(intersectionClusteredPoints.length) {
console.log(chalk.bold.keyword('blue')(' ✏️ Writing ' + intersectionClusteredPoints.length + ' intersection clustered points: ' + outFile + ".intersection_clustered.geojson"));
var intersectionClusteredPointsFeatureCollection:turfHelpers.FeatureCollection<turfHelpers.Point> = turfHelpers.featureCollection(intersectionClusteredPoints);
var intersectionClusteredJsonOut = JSON.stringify(intersectionClusteredPointsFeatureCollection);
writeFileSync(outFile + ".intersection_clustered.geojson", intersectionClusteredJsonOut);
}
if(unmatchedPoints.length ) {
console.log(chalk.bold.keyword('blue')(' ✏️ Writing ' + unmatchedPoints.length + ' unmatched points: ' + outFile + ".unmatched.geojson"));
var unmatchedFeatureCollection:turfHelpers.FeatureCollection<turfHelpers.Point> = turfHelpers.featureCollection(unmatchedPoints);
var unmatchedJsonOut = JSON.stringify(unmatchedFeatureCollection);
writeFileSync(outFile + ".unmatched.geojson", unmatchedJsonOut);
}
if(joinedPoints.length ) {
console.log(chalk.bold.keyword('blue')(' ✏️ Writing ' + joinedPoints.length + ' joined points: ' + outFile + ".joined.geojson"));
var joinedPointFeatureCollection:turfHelpers.FeatureCollection<turfHelpers.LineString> = turfHelpers.featureCollection(joinedPoints);
var joinedPointJson= JSON.stringify(joinedPointFeatureCollection);
writeFileSync(outFile + ".joined.geojson", joinedPointJson);
}
if(cleanPoints.invalid && cleanPoints.invalid.length > 0 ) {
console.log(chalk.bold.keyword('blue')(' ✏️ Writing ' + cleanPoints.invalid.length + ' invalid points: ' + outFile + ".invalid.geojson"));
var invalidJsonOut = JSON.stringify(cleanPoints.invalid );
writeFileSync(outFile + ".unmatched.geojson", invalidJsonOut);
}
}
enum MatchDirection {
FORWARD,
BACKWARD,
BOTH,
BEST
}
async function matchLines(outFile, params, lines, flags) {
var cleanedlines = new CleanedLines(lines);
console.log(chalk.bold.keyword('green')(' ✨ Matching ' + cleanedlines.clean.length + ' lines...'));
const getMatchedPath = (path:PathCandidate) => {
path.matchedPath.properties['segments'] = path.segments;
path.matchedPath.properties['score'] = path.score;
path.matchedPath.properties['matchType'] = path.matchType;
if(!flags['skip-port-properties'])
mapOgProperties(path.originalFeature.properties, path.matchedPath.properties);
return path.matchedPath;
}
const getMatchedSegments = (path:PathCandidate, ref:SharedStreetsReference) => {
var segmentIndex = 1;
var segmentGeoms = [];
for(var segment of path.segments) {
var segmentGeom = segment.geometry;
segmentGeom.properties = {};
segmentGeom.properties['shstReferenceId'] = segment.referenceId;
segmentGeom.properties['shstGeometryId'] = segment.geometryId;
segmentGeom.properties['shstFromIntersectionId'] = segment.fromIntersectionId;
segmentGeom.properties['shstToIntersectionId'] = segment.toIntersectionId;
segmentGeom.properties['referenceLength'] = segment.referenceLength;
segmentGeom.properties['section'] = segment.section;
segmentGeom.properties['gisReferenceId'] = ref.id;
segmentGeom.properties['gisGeometryId'] = ref.geometryId;
segmentGeom.properties['gisTotalSegments'] = path.segments.length
segmentGeom.properties['gisSegmentIndex'] = segmentIndex;
segmentGeom.properties['gisFromIntersectionId'] = ref.locationReferences[0].intersectionId;
segmentGeom.properties['gisToIntersectionId'] = ref.locationReferences[ref.locationReferences.length-1].intersectionId;
segmentGeom.properties['startSideOfStreet'] = path.startPoint.sideOfStreet;
segmentGeom.properties['endSideOfStreet'] = path.endPoint.sideOfStreet;
if(flags['side-of-street-field'] && path.originalFeature.properties[flags['side-of-street-field']]) {
var sideOfStreetValue = path.originalFeature.properties[flags['side-of-street-field']].toLocaleLowerCase();
if(flags['left-side-of-street-value'].toLocaleLowerCase() === sideOfStreetValue) {
path.sideOfStreet = ReferenceSideOfStreet.LEFT;
}
else if(flags['right-side-of-street-value'].toLocaleLowerCase() === sideOfStreetValue) {
path.sideOfStreet = ReferenceSideOfStreet.RIGHT;
}
else if(flags['center-of-street-value'].toLocaleLowerCase() === sideOfStreetValue) {
path.sideOfStreet = ReferenceSideOfStreet.CENTER;
}
else {
path.sideOfStreet = ReferenceSideOfStreet.UNKNOWN;
}
}
if(flags['offset-line']) {
if(flags['snap-side-of-street']) {
if(flags['left-side-driving']) {
if(path.sideOfStreet == ReferenceSideOfStreet.RIGHT)
segmentGeom = lineOffset(segmentGeom, 0 - flags['offset-line'], {"units":"meters"})
else if(path.sideOfStreet == ReferenceSideOfStreet.LEFT)
segmentGeom = lineOffset(segmentGeom, flags['offset-line'], {"units":"meters"})
}
else {
if(path.sideOfStreet == ReferenceSideOfStreet.RIGHT)
segmentGeom = lineOffset(segmentGeom, flags['offset-line'], {"units":"meters"})
else if(path.sideOfStreet == ReferenceSideOfStreet.LEFT)
segmentGeom = lineOffset(segmentGeom, 0 - flags['offset-line'], {"units":"meters"})
}
}
else {
if(flags['left-side-driving']) {
segmentGeom = lineOffset(segmentGeom, 0 - flags['offset-line'], {"units":"meters"});
}
else {
segmentGeom = lineOffset(segmentGeom, flags['offset-line'], {"units":"meters"});
}
}
}
segmentGeom.properties['sideOfStreet'] = path.sideOfStreet;
segmentGeom.properties['score'] = path.score;
segmentGeom.properties['matchType'] = path.matchType;
mapOgProperties(path.originalFeature.properties, segmentGeom.properties);
segmentGeoms.push(segmentGeom);
segmentIndex++;
}
return segmentGeoms;
};
var extent = envelope(lines);
var graphMode:GraphMode;
if(flags['match-bike'])
graphMode = GraphMode.BIKE;
else if(flags['match-pedestrian'])
graphMode = GraphMode.PEDESTRIAN;
else if(flags['match-car']) {
if(flags['match-motorway-only'])
graphMode = GraphMode.CAR_MOTORWAY_ONLY;
else if(flags['match-surface-only'])
graphMode = GraphMode.CAR_SURFACE_ONLY;
else
graphMode = GraphMode.CAR_ALL;
}
else
graphMode = GraphMode.CAR_ALL;
var matcher = new Graph(extent, params, graphMode);
await matcher.buildGraph();
if(flags['search-radius'])
matcher.searchRadius = flags['search-radius'];
matcher.snapIntersections = flags['snap-intersections'];
var matchedLines:turfHelpers.Feature<turfHelpers.LineString>[] = [];
var unmatchedLines:turfHelpers.Feature<turfHelpers.LineString>[] = [];
const bar1 = new cliProgress.Bar({},{
format: chalk.keyword('blue')(' {bar}') + ' {percentage}% | {value}/{total} ',
barCompleteChar: '\u2588',
barIncompleteChar: '\u2591'
});
bar1.start(cleanedlines.clean.length, 0);
for(var line of cleanedlines.clean) {
if(line.properties['geo_id'] == 30107269)
console.log('30107269')
var matchDirection:MatchDirection;
if(flags['direction-field'] && line.properties[flags['direction-field'].toLocaleLowerCase()] != undefined) {
var lineDirectionValue = '' + line.properties[flags['direction-field'].toLocaleLowerCase()];
if(lineDirectionValue == '' + flags['one-way-with-direction-value']) {
matchDirection = MatchDirection.FORWARD;
}
else if(lineDirectionValue == '' + flags['one-way-against-direction-value']) {
matchDirection = MatchDirection.BACKWARD;
}
else if(lineDirectionValue == '' + flags['two-way-value']) {
matchDirection = MatchDirection.BOTH;
}
else {
// TODO handle lines that don't match rules
matchDirection = MatchDirection.BOTH;
}
}
else if (flags['follow-line-direction']) {
matchDirection = MatchDirection.FORWARD;
}
else if (flags['best-direction']) {
matchDirection = MatchDirection.BEST;
}
else {
matchDirection = MatchDirection.BOTH;
}
var matchForward = null;
var matchForwardSegments = null;
if(matchDirection == MatchDirection.FORWARD || matchDirection == MatchDirection.BOTH || matchDirection == MatchDirection.BEST) {
var gisRef:SharedStreetsReference = forwardReference(line);
matchForward = await matcher.matchGeom(line);
if(matchForward && matchForward.score < matcher.searchRadius * 2) {
matchForwardSegments = getMatchedSegments(matchForward, gisRef);
}
}
var matchBackward = null;
var matchBackwardSegments = null;
if(matchDirection == MatchDirection.BACKWARD || matchDirection == MatchDirection.BOTH || matchDirection == MatchDirection.BEST) {
var gisRef:SharedStreetsReference = backReference(line);
var reversedLine = <turfHelpers.Feature<turfHelpers.LineString>>reverseLineString(line);
matchBackward = await matcher.matchGeom(reversedLine);
if(matchBackward && matchBackward.score < matcher.searchRadius * 2) {
matchBackwardSegments = getMatchedSegments(matchBackward, gisRef);
}
}
var matchedLine:boolean = false;
if((matchDirection == MatchDirection.FORWARD || matchDirection == MatchDirection.BOTH) && matchForwardSegments) {
matchedLines = matchedLines.concat(matchForwardSegments);
matchedLine = true;
}
if((matchDirection == MatchDirection.BACKWARD || matchDirection == MatchDirection.BOTH) && matchBackwardSegments) {
matchedLines = matchedLines.concat(matchBackwardSegments);
matchedLine = true;
}
if(matchDirection == MatchDirection.BEST) {
if(matchForward && matchBackward) {
if(matchForward.score > matchBackward.score) {
matchedLines = matchedLines.concat(matchForwardSegments);
matchedLine = true;
}
else if(matchForward.score == matchBackward.score) {
if(flags['left-side-driving']) {
if(matchForward.sideOfStreet == ReferenceSideOfStreet.LEFT)
matchedLines = matchedLines.concat(matchForwardSegments);
else
matchedLines = matchedLines.concat(matchBackwardSegments);
}
else {
if(matchForward.sideOfStreet == ReferenceSideOfStreet.RIGHT)
matchedLines = matchedLines.concat(matchForwardSegments);
else
matchedLines = matchedLines.concat(matchBackwardSegments);
}
matchedLine = true;
}
else {
matchedLines = matchedLines.concat(matchBackwardSegments);
matchedLine = true;
}
}
else if(matchForward) {
matchedLines = matchedLines.concat(matchForwardSegments);
matchedLine = true;
}
else if(matchBackward) {
matchedLines = matchedLines.concat(matchBackwardSegments);
matchedLine = true;
}
}
if(!matchedLine)
unmatchedLines.push(line);
bar1.increment();
}
bar1.stop();
if(matchedLines && matchedLines.length) {
console.log(chalk.bold.keyword('blue')(' ✏️ Writing ' + matchedLines.length + ' matched edges: ' + outFile + ".matched.geojson"));
var matchedFeatureCollection:turfHelpers.FeatureCollection<turfHelpers.LineString> = turfHelpers.featureCollection(matchedLines);
var matchedJsonOut = JSON.stringify(matchedFeatureCollection);
writeFileSync(outFile + ".matched.geojson", matchedJsonOut);
}
if(unmatchedLines && unmatchedLines.length ) {
console.log(chalk.bold.keyword('blue')(' ✏️ Writing ' + unmatchedLines.length + ' unmatched lines: ' + outFile + ".unmatched.geojson"));
var unmatchedFeatureCollection:turfHelpers.FeatureCollection<turfHelpers.LineString> = turfHelpers.featureCollection(unmatchedLines);
var unmatchedJsonOut = JSON.stringify(unmatchedFeatureCollection);
writeFileSync(outFile + ".unmatched.geojson", unmatchedJsonOut);
}
if(cleanedlines.invalid && cleanedlines.invalid.length ) {
console.log(chalk.bold.keyword('blue')(' ✏️ Writing ' + cleanedlines.invalid + ' in lines: ' + outFile + ".invalid.geojson"));
var invalidFeatureCollection:turfHelpers.FeatureCollection<turfHelpers.LineString> = turfHelpers.featureCollection(cleanedlines.invalid);
var invalidJsonOut = JSON.stringify(invalidFeatureCollection);
writeFileSync(outFile + ".unmatched.geojson", invalidJsonOut);
}
}