import circleToPolygon from 'circle-to-polygon' ; import pointInPoly from 'point-in-polygon' ; import { UUID , Timestamp , VehicleEvent , Telemetry , BoundingBox , Geography , Rule , EVENT_STATUS_MAP , VEHICLE_STATUS , BBox , VEHICLE_EVENT , SingleOrArray } from '@mds-core/mds-types' ; import logger from '@mds-core/mds-logger' ; import { MultiPolygon , Polygon , FeatureCollection , Geometry , Feature } from 'geojson' ; import { isArray } from 'util' ; import { getNextState } from './state-machine' ; import { parseRelative , getCurrentDate } from './date-time-utils' ; const RADIUS = 30.48 ; const NUMBER_OF_EDGES = 32 ; const UUID_REGEX = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/ ; function isUUID ( s ) { if ( typeof s !== 'string' ) { return false ; } return UUID_REGEX . test ( s ) ; } function round ( value , decimals ) { return Number ( `${ Math . round ( Number ( `${ typeof value !== 'number' ? parseFloat ( value ) : value }e${ decimals }` ) ) }e-${ decimals }` ) ; } function isPct ( val ) { if ( typeof val !== 'number' ) { return false ; } return val >= 0 && val <= 1 ; } function isTimestamp ( val ) { if ( typeof val !== 'number' ) { logger . info ( 'timestamp not an number' ) ; return false ; } if ( val < 1420099200000 ) { logger . info ( 'timestamp is prior to 1/1/2015; this is almost certainly seconds, not milliseconds' ) ; return false ; } return true ; } function seconds ( n ) { if ( typeof n !== 'number' ) { throw new Error ( 'timespan must be a number' ) ; } return n * 1000 ; } function minutes ( n ) { return seconds ( n ) * 60 ; } function hours ( n ) { return minutes ( n ) * 60 ; } function days ( n ) { return hours ( n ) * 24 ; } function timeframe ( size , timestamp ) { const start_time = timestamp - ( timestamp % size ) ; return { start_time , end_time : start_time + size - 1 } ; } param { number } minimum param { number } maximum return { number } function rangeRandom ( min , max ) { if ( typeof min !== 'number' || typeof max !== 'number' ) { throw new Error ( `rangeRandom: min, max must be numbers, not ${ min }, ${ max }` ) ; } return min + ( max - min ) * Math . random ( ) ; } param { number } minimum param { number } maximum return { number } function rangeRandomInt ( min , max ? ) { if ( max === undefined ) { return Math . floor ( rangeRandom ( 0 , min ) ) ; } return Math . floor ( rangeRandom ( min , max ) ) ; } function randomElement < T > ( list ) { return list [ rangeRandomInt ( list . length ) ] ; } function head < T > ( list ) { if ( ! Array . isArray ( list ) ) { throw new Error ( 'not a list' ) ; } return list [ 0 ] ; } function tail < T > ( list ) { if ( ! Array . isArray ( list ) ) { throw new Error ( 'not a list' ) ; } return list [ list . length - 1 ] ; } param { MultiPolygon } return { bbox } function calcBBox ( geometry ) { let latMin = 10000 ; let latMax = - 10000 ; let lngMin = 10000 ; let lngMax = - 10000 ; function expand ( poly ) { if ( ! Array . isArray ( poly ) ) { throw new Error ( 'poly is not a list' ) ; } if ( typeof poly [ 0 ] [ 0 ] !== 'number' || typeof poly [ 0 ] [ 1 ] !== 'number' ) { throw new Error ( 'poly is not a list of [num,num]' ) ; } for ( const pair of poly ) { const [ lng , lat ] = pair ; if ( latMin > lat ) { latMin = lat ; } if ( latMax < lat ) { latMax = lat ; } if ( lngMin > lng ) { lngMin = lng ; } if ( lngMax < lng ) { lngMax = lng ; } } } let poly ; switch ( geometry . type ) { case 'Polygon' : ; [ poly ] = geometry . coordinates ; expand ( poly ) ; break ; case 'MultiPolygon' : { const coords = geometry . coordinates ; for ( const polyWithHoles of coords ) { ; [ poly ] = polyWithHoles ; expand ( poly ) ; } } break ; default : throw new Error ( `calcBBox does not (yet) handle geometry of type ${ geometry . type }` ) ; } return { latMin , latMax , lngMin , lngMax } ; } function pointInPolyWithHoles ( pt , polyWithHoles ) { const poly = polyWithHoles [ 0 ] ; if ( pointInPoly ( pt , poly ) ) { const holes = polyWithHoles . slice ( 1 ) ; for ( const hole of holes ) { if ( pointInPoly ( pt , hole ) ) { return false ; } } return true ; } return false ; } param { Point or param { MultiPolygon } return { true function pointInMultiPolygon ( pt , multipoly ) { return multipoly . coordinates . some ( ( polyWithHoles ) => pointInPolyWithHoles ( pt , polyWithHoles ) ) ; } param { Point or param { Polygon } return { true function pointInPolygon ( pt , poly ) { return pointInPolyWithHoles ( pt , poly . coordinates ) ; } function pointInGeometry ( pt , shape ) { if ( shape . type === 'MultiPolygon' ) { return pointInMultiPolygon ( pt , shape ) ; } if ( shape . type === 'Polygon' ) { return pointInPolygon ( pt , shape ) ; } if ( shape . type === 'Point' ) { if ( pointInPolygon ( pt , circleToPolygon ( shape . coordinates , RADIUS , NUMBER_OF_EDGES ) ) ) { return true ; } return false ; } throw new Error ( `cannot check point in shape for type ${ shape . type }` ) ; } function pointInFeatureCollection ( pt , fc ) { return fc . features . some ( ( feature ) => pointInGeometry ( pt , feature . geometry ) ) ; } function pointInShape ( pt , shape ) { const point = Array . isArray ( pt ) ? pt : [ pt . lng , pt . lat ] ; if ( shape . type === 'Point' ) { if ( pointInPolygon ( point , circleToPolygon ( shape . coordinates , RADIUS , NUMBER_OF_EDGES ) ) ) { return true ; } return false ; } if ( shape . type === 'MultiPolygon' ) { return pointInMultiPolygon ( point , shape ) ; } if ( shape . type === 'Polygon' ) { return pointInPolygon ( point , shape ) ; } if ( shape . type === 'FeatureCollection' ) { return pointInFeatureCollection ( point , shape ) ; } return pointInGeometry ( point , shape ) ; } param { MultiPolygon } return { Point } function makePointInShape ( shape ) { if ( ! shape ) { throw new Error ( 'no shape' ) ; } const shapeToCreate = shape . type === 'Point' ? circleToPolygon ( shape . coordinates , RADIUS , NUMBER_OF_EDGES ) : shape ; const bbox = calcBBox ( shapeToCreate ) ; let tries = 0 ; while ( tries < 1000 ) { const pt = [ rangeRandom ( bbox . lngMin , bbox . lngMax ) , rangeRandom ( bbox . latMin , bbox . latMax ) ] ; if ( pointInShape ( pt , shapeToCreate ) ) { return { lng : pt [ 0 ] , lat : pt [ 1 ] } ; } tries += 1 ; } throw new Error ( 'tried 1000 times to put a point in poly and failed' ) ; } param { [ type ] } _deg return { [ type ] } function rad ( _deg ) { return ( _deg * Math . PI ) / 180 ; } param { [ type ] } _rad return { [ type ] } function deg ( _rad ) { return ( _rad * 180 ) / Math . PI ; } param { Point } param { distance in param { bearing in function addDistanceBearing < T extends { lat : number ; lng : number ; } > ( pt , distance , bearing ) { if ( Number . isNaN ( distance ) ) { throw new Error ( 'bad distance' ) ; } if ( Number . isNaN ( bearing ) ) { throw new Error ( 'bad bearing' ) ; } const R = 6378100 ; const B = rad ( bearing ) ; const lat1 = rad ( pt . lat ) ; const lng1 = rad ( pt . lng ) ; const xx = Math . sin ( lat1 ) * Math . cos ( distance / R ) + Math . cos ( lat1 ) * Math . sin ( distance / R ) * Math . cos ( B ) ; const lat2 = Math . asin ( xx ) ; const num = Math . sin ( B ) * Math . sin ( distance / R ) * Math . cos ( lat1 ) ; const den = Math . cos ( distance / R ) - Math . sin ( lat1 ) * Math . sin ( lat2 ) ; const lng2 = lng1 + Math . atan2 ( num , den ) ; return { ... pt , lat : deg ( lat2 ) , lng : deg ( lng2 ) } ; } param { [ type ] } array param { [ type ] } size return { [ type ] } function getRandomSubarray < T > ( array , size ) { const shuffled = array . slice ( ) ; const min = array . length - size ; for ( let i = array . length ; i > min ; i -= 1 ) { const index = Math . floor ( ( i + 1 ) * Math . random ( ) ) ; const temp = shuffled [ index ] ; shuffled [ index ] = shuffled [ i ] ; shuffled [ i ] = temp ; } return shuffled . slice ( min ) ; } param { string describing return { bounding function parseBBox ( bbox_str ) { if ( ! bbox_str || typeof bbox_str !== 'string' ) { return null ; } const parts = bbox_str . replace ( /[[\] ]+/g , '' ) . split ( ',' ) . map ( parseFloat ) ; if ( parts . length !== 4 ) { return null ; } const [ lng1 , lat1 , lng2 , lat2 ] = parts ; return { lngMin : Math . min ( lng1 , lng2 ) , lngMax : Math . max ( lng1 , lng2 ) , latMin : Math . min ( lat1 , lat2 ) , latMax : Math . max ( lat1 , lat2 ) } ; } function capitalizeFirst ( s ) { return s . charAt ( 0 ) . toUpperCase ( ) + s . slice ( 1 ) ; } function nullKeys < T > ( obj ) { return Object . keys ( obj ) . filter ( key => obj [ key ] === null || obj [ key ] === undefined ) ; } function stripNulls < T extends { } > ( obj ) { return nullKeys ( obj ) . reduce ( ( stripped , key ) => { const { [ key ] : nullkey , ... rest } = stripped ; return rest ; } , obj ) as Partial < T > ; } function isFloat ( n ) { return typeof n === 'number' || ( typeof n === 'string' && ! Number . isNaN ( parseFloat ( n ) ) ) ; } function now ( ) { return Date . now ( ) ; } function range ( n ) { return [ ... Array ( n ) . keys ( ) ] ; } function nonNegInt ( val , def ) { if ( def === undefined ) { throw new Error ( 'nonNegInt: no default value' ) ; } if ( val === undefined ) { return def ; } const parsed = parseInt ( val ) ; if ( Number . isInteger ( parsed ) && parsed >= 0 ) { return parsed ; } throw new Error ( `invalid nonNegInt "${ val }"` ) ; } function yesterday ( ) { return Date . now ( ) - days ( 1 ) ; } function csv < T > ( list ) { return list . join ( ', ' ) ; } function inc ( map , key ) { return Object . assign ( map , { [ key ] : map [ key ] ? map [ key ] + 1 : 1 } ) ; } function pathsFor ( path ) { const { PATH_PREFIX } = process . env ; return [ path , PATH_PREFIX + path , `${ PATH_PREFIX }/dev${ path }` ] ; } function isInsideBoundingBox ( telemetry , bbox ) { if ( telemetry && telemetry . gps ) { const { lat , lng } = telemetry . gps ; if ( ! lat || ! lng ) { return false ; } const [ [ lng1 , lat1 ] , [ lng2 , lat2 ] ] = bbox ; const latMin = Math . min ( lat1 , lat2 ) ; const latMax = Math . max ( lat1 , lat2 ) ; const lngMin = Math . min ( lng1 , lng2 ) ; const lngMax = Math . max ( lng1 , lng2 ) ; return latMin <= lat && lat <= latMax && lngMin <= lng && lng <= lngMax ; } return false ; } function isStateTransitionValid ( eventA , eventB ) { const currState = EVENT_STATUS_MAP [ eventA . event_type ] ; const nextState = getNextState ( currState , eventB . event_type ) ; return nextState !== undefined ; } function getPolygon ( geographies , geography ) { const res = geographies . find ( ( location ) => { return location . geography_id === geography ; } ) ; if ( res === undefined ) { throw new Error ( `Geography ${ geography } not found in ${ geographies }!` ) ; } return res . geography_json ; } function isInStatesOrEvents ( rule , event ) { const status = rule . statuses ? rule . statuses [ EVENT_STATUS_MAP [ event . event_type ] as VEHICLE_STATUS ] : null ; return status !== null ? rule . statuses !== null && Object . keys ( rule . statuses ) . includes ( EVENT_STATUS_MAP [ event . event_type ] ) && status !== undefined && ( status . length === 0 || ( status as string [ ] ) . includes ( event . event_type ) ) : true ; } function routeDistance ( coordinates ) { const R = 6371000 ; return ( coordinates || [ ] ) . map ( coordinate => [ rad ( coordinate . lat ) , rad ( coordinate . lng ) ] ) . reduce ( ( distance , point , index , points ) => { if ( index > 0 ) { const [ lat1 , lng1 ] = points [ index - 1 ] ; const [ lat2 , lng2 ] = point ; const [ dlat , dlng ] = [ lat2 - lat1 , lng2 - lng1 ] ; const a = Math . sin ( dlat / 2 ) ** 2 + Math . cos ( lat1 ) * Math . cos ( lat2 ) * Math . sin ( dlng / 2 ) ** 2 ; const c = 2 * Math . atan2 ( Math . sqrt ( a ) , Math . sqrt ( 1 - a ) ) ; const d = R * c ; return distance + d ; } return distance ; } , 0 ) ; } function clone < T > ( obj ) { return JSON . parse ( JSON . stringify ( obj ) ) ; } type isDefinedOptions = Partial < { warnOnEmpty : boolean ; } > ; const isDefined = < T > ( elem , options = { } , index ? ) : elem is T => { const { warnOnEmpty } = options ; if ( elem !== undefined && elem !== null ) { return true ; } if ( warnOnEmpty ) { logger . warn ( `Encountered empty element at index: ${ index }` ) ; } return false ; } ; const filterDefined = ( options = { } ) => < T > ( value , index , array ) : value is T => isDefined ( value , options , index ) ; function moved ( latA , lngA , latB , lngB ) { const limit = 0.00001 ; const latDiff = Math . abs ( latA - latB ) ; const lngDiff = Math . abs ( lngA - lngB ) ; return lngDiff > limit || latDiff > limit ; } function normalizeToArray < T > ( elementToNormalize ) { if ( elementToNormalize === undefined ) { return [ ] ; } if ( isArray ( elementToNormalize ) ) { return elementToNormalize ; } return [ elementToNormalize ] ; } const getEnvVar = < TProps extends { [ name ] : string ; } > ( props ) : TProps => Object . keys ( props ) . reduce ( ( env , key ) => { return { ... env , [ key ] : process . env [ key ] || props [ key ] } ; } , { } as TProps ) ; export type ParseObjectPropertiesOptions < T > = Partial < { parser : ( value : string ) => T ; } > ; const parseObjectProperties = < T = string > ( obj , { parser } = { } ) => { return { keys : < TKey extends string > ( first , ... rest ) : Partial < { [ P in TKey ] : T ; } > => [ first , ... rest ] . map ( key => ( { key , value : obj [ key ] } ) ) . filter ( ( param ) : param is { key : TKey ; value : string ; } => typeof param . value === 'string' ) . reduce ( ( params , { key , value } ) => ( { ... params , [ key ] : parser ? parser ( value ) : value } ) , { } ) } ; } ; const asArray = < T > ( value ) : T [ ] => ( Array . isArray ( value ) ? value : [ value ] ) ; const pluralize = ( count , singular , plural ) => ( count === 1 ? singular : plural ) ; export { UUID_REGEX , isUUID , isPct , isTimestamp , rangeRandom , rangeRandomInt , randomElement , addDistanceBearing , pointInShape , makePointInShape , getRandomSubarray , round , parseBBox , capitalizeFirst , nullKeys , stripNulls , isFloat , now , range , nonNegInt , days , hours , minutes , seconds , timeframe , yesterday , csv , inc , pathsFor , isInsideBoundingBox , head , tail , isStateTransitionValid , pointInGeometry , getPolygon , isInStatesOrEvents , routeDistance , clone , isDefined , moved , normalizeToArray , parseRelative , getCurrentDate , getEnvVar , parseObjectProperties , asArray , pluralize , filterDefined } ;