import { Compliance , ComplianceResponse , CountMatch , CountRule , Device , Geography , Policy , Rule , SpeedMatch , SpeedRule , TimeMatch , TimeRule , VehicleEvent , MatchedVehicle , EVENT_STATUS_MAP , RULE_UNIT_MAP , DAY_OF_WEEK , VEHICLE_STATUS , TIME_FORMAT , DAYS_OF_WEEK , UUID } from '@mds-core/mds-types' ; import { pointInShape , getPolygon , isInStatesOrEvents , now , RuntimeError } from '@mds-core/mds-utils' ; import moment from 'moment-timezone' ; import { MatchedVehiclePlusRule , VehicleEventWithTelemetry } from './types' ; const { env } = process ; const TWO_DAYS_IN_MS = 172800000 ; function isPolicyActive ( policy , end_time = now ( ) ) { if ( policy . end_date === null ) { return end_time >= policy . start_date ; } return end_time >= policy . start_date && end_time <= policy . end_date ; } function isRuleActive ( rule ) { if ( ! env . TIMEZONE ) { throw new RuntimeError ( 'TIMEZONE environment variable must be declared!' ) ; } if ( ! moment . tz . names ( ) . includes ( env . TIMEZONE ) ) { throw new RuntimeError ( `TIMEZONE environment variable ${ env . TIMEZONE } is not a valid timezone!` ) ; } const local_time = moment ( ) . tz ( env . TIMEZONE ) ; if ( ! rule . days || rule . days . includes ( Object . values ( DAYS_OF_WEEK ) [ local_time . day ( ) ] as DAY_OF_WEEK ) ) { if ( ! rule . start_time || local_time . isAfter ( moment ( rule . start_time , TIME_FORMAT ) ) ) { if ( ! rule . end_time || local_time . isBefore ( moment ( rule . end_time , TIME_FORMAT ) ) ) { return true ; } } } return false ; } function isInVehicleTypes ( rule , device ) { return ! rule . vehicle_types || ( rule . vehicle_types && rule . vehicle_types . includes ( device . type ) ) ; } function getMatchedVehicleFromDeviceAndEvent ( device , event ) { return { device_id : device . device_id , provider_id : device . provider_id , vehicle_id : device . vehicle_id , vehicle_type : device . type , vehicle_status : EVENT_STATUS_MAP [ event . event_type ] as VEHICLE_STATUS , gps : { lat : event . telemetry . gps . lat , lng : event . telemetry . gps . lng } } ; } function getViolationsArray ( map ) { return Object . keys ( map ) . map ( device_id => { const { rule_id } = map [ device_id ] ; return { device_id , rule_id } ; } ) ; } function processCountRule ( rule , events , geographies , devices ) { const maximum = rule . maximum || Number . POSITIVE_INFINITY ; if ( isRuleActive ( rule ) ) { const matches = rule . geographies . reduce ( ( matches_acc , geography ) : CountMatch [ ] => { const matched_vehicles = events . reduce ( ( matched_vehicles_acc , event ) : MatchedVehicle [ ] => { const device = devices [ event . device_id ] ; if ( event . telemetry && device ) { if ( isInStatesOrEvents ( rule , event ) && isInVehicleTypes ( rule , device ) ) { const poly = getPolygon ( geographies , geography ) ; if ( poly && pointInShape ( event . telemetry . gps , poly ) ) { matched_vehicles_acc . push ( getMatchedVehicleFromDeviceAndEvent ( device , event as VehicleEventWithTelemetry ) ) ; } } } return matched_vehicles_acc ; } , [ ] ) ; matches_acc . push ( { geography_id : geography , measured : maximum && matched_vehicles . length > maximum ? maximum : matched_vehicles . length , matched_vehicles } ) ; return matches_acc ; } , [ ] ) ; return { rule , matches } ; } return { rule , matches : [ ] } ; } function processTimeRule ( rule , events , geographies , devices ) { if ( isRuleActive ( rule ) ) { const matches = rule . geographies . reduce ( ( matches_acc , geography ) : TimeMatch [ ] => { events . forEach ( ( event ) => { const device = devices [ event . device_id ] ; if ( event . telemetry && device ) { if ( isInStatesOrEvents ( rule , event ) && isInVehicleTypes ( rule , device ) && ( ! rule . maximum || ( now ( ) - event . timestamp ) / RULE_UNIT_MAP [ rule . rule_units ] >= rule . maximum ) ) { const poly = getPolygon ( geographies , geography ) ; if ( poly && pointInShape ( event . telemetry . gps , poly ) ) { matches_acc . push ( { measured : now ( ) - event . timestamp , geography_id : geography , matched_vehicle : getMatchedVehicleFromDeviceAndEvent ( device , event as VehicleEventWithTelemetry ) } ) ; } } } } ) ; return matches_acc ; } , [ ] ) ; return { rule , matches } ; } return { rule , matches : [ ] } ; } function processSpeedRule ( rule , events , geographies , devices ) { if ( isRuleActive ( rule ) ) { const matches_result = rule . geographies . reduce ( ( matches , geography ) => { events . forEach ( ( event ) => { const device = devices [ event . device_id ] ; if ( event . telemetry && device ) { if ( isInStatesOrEvents ( rule , event ) && isInVehicleTypes ( rule , device ) && event . telemetry . gps . speed && pointInShape ( event . telemetry . gps , getPolygon ( geographies , geography ) ) && ( ! rule . maximum || event . telemetry . gps . speed >= rule . maximum ) ) { matches . push ( { measured : event . telemetry . gps . speed , geography_id : geography , matched_vehicle : getMatchedVehicleFromDeviceAndEvent ( device , event as VehicleEventWithTelemetry ) } ) ; } } } ) ; return matches ; } , [ ] ) ; return { rule , matches : matches_result } ; } return { rule , matches : [ ] } ; } function processPolicy ( policy , events , geographies , devices ) { if ( isPolicyActive ( policy ) ) { const sortedEvents = events . sort ( ( e_1 , e_2 ) => { return e_1 . timestamp - e_2 . timestamp ; } ) ; const vehiclesToFilter = [ ] ; let overflowVehiclesMap = { } ; let countVehiclesMap = { } ; let countViolations = 0 ; const timeVehiclesMap = { } ; const speedingVehiclesMap = { } ; const compliance = policy . rules . reduce ( ( compliance_acc , rule ) : Compliance [ ] => { vehiclesToFilter . forEach ( ( vehicle ) => { delete devices [ vehicle . device_id ] ; } ) ; switch ( rule . rule_type ) { case 'count' : { const comp = processCountRule ( rule , sortedEvents , geographies , devices ) ; const compressedComp = { rule , matches : comp . matches ? comp . matches . reduce ( ( acc , inst ) => { const { measured , geography_id } = inst ; return [ ... acc , { measured , geography_id } ] ; } , [ ] ) : [ ] } ; const bucketMap = comp . matches . reduce ( ( acc2 , match ) => { return [ ... acc2 , match . matched_vehicles . reduce ( ( acc , match_instance , i ) => { const maximum = rule . maximum == null ? Number . POSITIVE_INFINITY : rule . maximum ; if ( maximum && i < maximum ) { if ( overflowVehiclesMap [ match_instance . device_id ] ) { delete overflowVehiclesMap [ match_instance . device_id ] ; } if ( countVehiclesMap [ match_instance . device_id ] ) { delete countVehiclesMap [ match_instance . device_id ] ; } acc . matched . push ( match_instance ) ; } else { acc . overflowed . push ( { ... match_instance , rule_id : rule . rule_id } ) ; } return acc ; } , { matched : [ ] , overflowed : [ ] } ) ] ; } , [ { matched : [ ] , overflowed : [ ] } ] ) ; const vehiclesMatched = bucketMap . reduce ( ( acc , map ) => { return [ ... acc , ... map . matched ] ; } , [ ] ) ; vehiclesToFilter . push ( ... vehiclesMatched ) ; const overflowVehicles = bucketMap . reduce ( ( acc , map ) => { return [ ... acc , ... map . overflowed ] ; } , [ ] ) ; overflowVehiclesMap = { ... overflowVehiclesMap , ... overflowVehicles . reduce ( ( acc , device ) => { acc [ device . device_id ] = device ; return acc ; } , { } ) } ; const minimum = rule . minimum == null ? Number . NEGATIVE_INFINITY : rule . minimum ; if ( overflowVehicles . length > 0 ) { countVehiclesMap = { ... countVehiclesMap , ... overflowVehiclesMap } ; countViolations += overflowVehicles . length ; } else if ( vehiclesMatched . length < minimum ) { countViolations += minimum - vehiclesMatched . length ; } compliance_acc . push ( compressedComp ) ; break ; } case 'time' : { const comp = processTimeRule ( rule , sortedEvents , geographies , devices ) ; compliance_acc . push ( comp ) ; const timeVehicles = comp . matches ? comp . matches . reduce ( ( acc , match ) => { const vehicle = match . matched_vehicle ; timeVehiclesMap [ vehicle . device_id ] = { ... vehicle , ... { rule_id : rule . rule_id } } ; acc . push ( vehicle ) ; return acc ; } , [ ] ) : [ ] ; vehiclesToFilter . push ( ... timeVehicles ) ; break ; } case 'speed' : { const comp = processSpeedRule ( rule , sortedEvents , geographies , devices ) ; compliance_acc . push ( comp ) ; const speedingVehicles = comp . matches ? comp . matches . reduce ( ( acc , match ) => { acc . push ( match . matched_vehicle ) ; return acc ; } , [ ] ) : [ ] ; vehiclesToFilter . push ( ... speedingVehicles ) ; speedingVehicles . forEach ( vehicle => { speedingVehiclesMap [ vehicle . device_id ] = { ... vehicle , ... { rule_id : rule . rule_id } } ; } ) ; } break ; default : compliance_acc . push ( { rule , matches : [ ] } ) ; } return compliance_acc ; } , [ ] ) ; const countVehicles = getViolationsArray ( countVehiclesMap ) ; const timeVehicles = getViolationsArray ( timeVehiclesMap ) ; const speedingVehicles = getViolationsArray ( speedingVehiclesMap ) ; return { policy , compliance , total_violations : countViolations + timeVehicles . length + speedingVehicles . length , vehicles_in_violation : { ... countVehicles , ... timeVehiclesMap , ... speedingVehiclesMap } } ; } } function filterPolicies ( policies ) { const prev_policies = policies . reduce ( ( prev_policies_acc , policy ) => { if ( policy . prev_policies ) { prev_policies_acc . push ( ... policy . prev_policies ) ; } return prev_policies_acc ; } , [ ] ) ; return policies . filter ( ( policy ) => { return ! prev_policies . includes ( policy . policy_id ) ; } ) ; } function filterEvents ( events , end_time = now ( ) ) { return events . filter ( ( event ) => { return event . timestamp > end_time - TWO_DAYS_IN_MS ; } ) ; } export { processPolicy , filterPolicies , filterEvents } ;