UNPKG

sharedstreets

Version:

SharedStreets, a 'digital commons' for the street

969 lines (761 loc) 43.7 kB
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); } }