diff --git a/README.md b/README.md index 0c81b05..ee4a6e4 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,10 @@ A `config.json` file is required to run Mobility Metrics. Enable providers and s - maximum number of days without status change before vehicles are permanently lost - `summary` - enabled or disabled metrics in summary UI +- `vehicleFilter` + - optionally allow only one type of [MDS vehicle_type](https://github.com/openmobilityfoundation/mobility-data-specification/tree/dev/provider#vehicle-types) +- `geographicFilter` + - filter all data that falls outside the defined geographic filter, formatted as a valid GeoJSON Feature of type Polygon or MultiPolygon - `providers` - list of providers to query - `type` diff --git a/example/example.json b/example/example.json index 3a14d2b..a03fd58 100644 --- a/example/example.json +++ b/example/example.json @@ -9,14 +9,30 @@ "zoom": 12.75, "privacyMinimum": 3, "lost": 2, + "geographicFilter": { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-86.77564144134521, 36.163344537460084], + [-86.74620151519775, 36.163344537460084], + [-86.74620151519775, 36.17997342339555], + [-86.77564144134521, 36.17997342339555], + [-86.77564144134521, 36.163344537460084] + ] + ] + } + }, "summary": { "Unique Vehicles": true, "Active Vehicles": true, "Total Trips": true, "Total Trip Distance": true, - "Distance Per Vehicle": false, + "Distance Per Vehicle": true, "Vehicle Utilization": true, - "Trips Per Active Vehicle": false, + "Trips Per Active Vehicle": true, "Avg Trip Distance": true, "Avg Trip Duration": true }, diff --git a/example/simulate.js b/example/simulate.js index 0871be3..d946493 100644 --- a/example/simulate.js +++ b/example/simulate.js @@ -27,7 +27,7 @@ exec("../node_modules/osrm/lib/binding/osrm-contract graph/nash.osrm"); const providers = ["Flipr", "Scoob", "BikeMe", "Spuun"]; -const days = 30; +const days = 2; const start = 1563087600000; // Sunday, July 14, 2019 3:00:00 AM GMT-04:00 @@ -42,8 +42,8 @@ cmd += "--changes data/{provider}/changes.json "; cmd += "--trips data/{provider}/trips.json "; cmd += "--quiet "; -const minAgents = 100; -const maxAgents = 300; +const minAgents = 50; +const maxAgents = 200; console.log("running simulations..."); diff --git a/src/cli.js b/src/cli.js index 3f053a5..3d60f06 100755 --- a/src/cli.js +++ b/src/cli.js @@ -37,17 +37,53 @@ if (argv.help || argv.h || Object.keys(argv).length === 1) { const config = require(path.resolve(argv.config)); // add spatial indices to zones +if (!config.zones) { + config.zones = turf.FeatureCollection([]); +} const z = 19; const zs = { min_zoom: z, max_zoom: z }; -if (config.zones) { - for (let zone of config.zones.features) { - zone.properties.keys = {}; - const keys = cover.indexes(zone.geometry, zs); - for (let key of keys) { - zone.properties.keys[key] = 1; - } +for (let zone of config.zones.features) { + zone.properties.keys = {}; + const keys = cover.indexes(zone.geometry, zs); + for (let key of keys) { + zone.properties.keys[key] = 1; } } +// build geographicFilter lookup +if (config.geographicFilter) { + config.geographicFilterKeys = {}; + cover.indexes(config.geographicFilter.geometry, zs).forEach(qk => { + config.geographicFilterKeys[qk] = 1; + }); +} + +// check for valid vehicleFilter +if ( + config.vehicleFilter && + (config.vehicleFilter !== "car" && + config.vehicleFilter !== "bicycle" && + config.vehicleFilter !== "scooter") +) { + throw new Error("detected invalid vehicle filter"); +} + +// defaults +if (!config.zoom) config.zoom = 12.5; +if (!config.lost) config.lost = 2; +if (!config.privacyMinimum || config.privacyMinimum < 3) + config.privacyMinimum = 3; +if (!config.summary) + config.summary = { + "Unique Vehicles": true, + "Active Vehicles": true, + "Total Trips": true, + "Total Trip Distance": true, + "Distance Per Vehicle": true, + "Vehicle Utilization": true, + "Trips Per Active Vehicle": true, + "Avg Trip Distance": true, + "Avg Trip Duration": true + }; const publicPath = path.resolve(argv.public); const cachePath = path.resolve(argv.cache); diff --git a/src/matchers/change.js b/src/matchers/change.js index 2da3608..985b350 100644 --- a/src/matchers/change.js +++ b/src/matchers/change.js @@ -6,50 +6,62 @@ const z = 19; const zs = { min_zoom: z, max_zoom: z }; module.exports = async function(change, config, graph) { - // STREETS + if (!config.vehicleFilter || config.vehicleFilter === change.vehicle_type) { + const keys = cover.indexes( + turf.point(change.event_location.geometry.coordinates).geometry, + zs + ); - if (!change.matches) change.matches = {}; + if (config.geographicFilter) { + var pass = false; + for (let key of keys) { + if (config.geographicFilterKeys[key]) { + pass = true; + } + } + if (!pass) return; + } - const matches = await graph.matchPoint(change.event_location, null, 1); + // STREETS + if (!change.matches) change.matches = {}; + const matches = await graph.matchPoint(change.event_location, null, 1); - if (matches.length) { - change.matches.streets = matches; - } + if (matches.length) { + change.matches.streets = matches; + } - // BINS + // BINS - const bin = h3.geoToH3( - change.event_location.geometry.coordinates[1], - change.event_location.geometry.coordinates[0], - config.Z - ); + const bin = h3.geoToH3( + change.event_location.geometry.coordinates[1], + change.event_location.geometry.coordinates[0], + config.Z + ); - change.matches.bins = bin; + change.matches.bins = bin; - // ZONES + // ZONES - if (config.zones) { - var zoneMatches = []; - const keys = cover.indexes( - turf.point(change.event_location.geometry.coordinates).geometry, - zs - ); - for (let zone of config.zones.features) { - let found = false; - for (let key of keys) { - if (zone.properties.keys[key]) found = true; - continue; + if (config.zones) { + var zoneMatches = []; + + for (let zone of config.zones.features) { + let found = false; + for (let key of keys) { + if (zone.properties.keys[key]) found = true; + continue; + } + + if (found) { + zoneMatches.push(zone.properties.id); + } } - if (found) { - zoneMatches.push(zone.properties.id); + if (zoneMatches.length) { + change.matches.zones = zoneMatches; } } - if (zoneMatches.length) { - change.matches.zones = zoneMatches; - } + return change; } - - return change; }; diff --git a/src/matchers/trip.js b/src/matchers/trip.js index 04add7a..81cd891 100644 --- a/src/matchers/trip.js +++ b/src/matchers/trip.js @@ -6,103 +6,117 @@ const z = 19; const zs = { min_zoom: z, max_zoom: z }; module.exports = async function(trip, config, graph) { - const line = turf.lineString( - trip.route.features.map(pt => { - return pt.geometry.coordinates; - }) - ); - - // STREETS - - if (!trip.matches) trip.matches = {}; - - const match = await graph.matchTrace(line); - - if ( - match && - match.segments && - match.matchedPath && - match.matchedPath.geometry && - match.matchedPath.geometry.coordinates && - match.matchedPath.geometry.coordinates.length === match.segments.length - ) { - trip.matches.streets = match; - } - - // HEXES - - var bins = {}; - trip.route.features.forEach(ping => { - var bin = h3.geoToH3( - ping.geometry.coordinates[1], - ping.geometry.coordinates[0], - config.Z + if (!config.vehicleFilter || config.vehicleFilter === trip.vehicle_type) { + const line = turf.lineString( + trip.route.features.map(pt => { + return pt.geometry.coordinates; + }) ); - bins[bin] = 1; - }); - trip.matches.bins = bins; - - // ZONES - - if (config.zones) { - // trace - var zoneMatches = []; const keys = cover.indexes(line.geometry, zs); - for (let zone of config.zones.features) { - let found = false; - for (let key of keys) { - if (zone.properties.keys[key]) found = true; - continue; - } - if (found) { - zoneMatches.push(zone.properties.id); + if (config.geographicFilter) { + var pass = false; + for (let key of keys) { + if (config.geographicFilterKeys[key]) { + pass = true; + } } + if (!pass) return; } - if (zoneMatches.length) { - trip.matches.zones = zoneMatches; + // STREETS + + if (!trip.matches) trip.matches = {}; + + const match = await graph.matchTrace(line); + + if ( + match && + match.segments && + match.matchedPath && + match.matchedPath.geometry && + match.matchedPath.geometry.coordinates && + match.matchedPath.geometry.coordinates.length === match.segments.length + ) { + trip.matches.streets = match; } - // pickup - var pickupZoneMatches = []; - const pickupKeys = cover.indexes(line.geometry, zs); - for (let zone of config.zones.features) { - let found = false; - for (let key of pickupKeys) { - if (zone.properties.keys[key]) found = true; - continue; + // HEXES + + var bins = {}; + trip.route.features.forEach(ping => { + var bin = h3.geoToH3( + ping.geometry.coordinates[1], + ping.geometry.coordinates[0], + config.Z + ); + bins[bin] = 1; + }); + + trip.matches.bins = bins; + + // ZONES + + if (config.zones) { + // trace + var zoneMatches = []; + + for (let zone of config.zones.features) { + let found = false; + for (let key of keys) { + if (zone.properties.keys[key]) found = true; + continue; + } + + if (found) { + zoneMatches.push(zone.properties.id); + } } - if (found) { - pickupZoneMatches.push(zone.properties.id); + if (zoneMatches.length) { + trip.matches.zones = zoneMatches; } - } - if (pickupZoneMatches.length) { - trip.matches.pickupZones = pickupZoneMatches; - } + // pickup + var pickupZoneMatches = []; + const pickupKeys = cover.indexes(line.geometry, zs); + for (let zone of config.zones.features) { + let found = false; + for (let key of pickupKeys) { + if (zone.properties.keys[key]) found = true; + continue; + } + + if (found) { + pickupZoneMatches.push(zone.properties.id); + } + } - // dropoff - var dropoffZoneMatches = []; - const dropoffKeys = cover.indexes(line.geometry, zs); - for (let zone of config.zones.features) { - let found = false; - for (let key of dropoffKeys) { - if (zone.properties.keys[key]) found = true; - continue; + if (pickupZoneMatches.length) { + trip.matches.pickupZones = pickupZoneMatches; } - if (found) { - dropoffZoneMatches.push(zone.properties.id); + // dropoff + var dropoffZoneMatches = []; + const dropoffKeys = cover.indexes(line.geometry, zs); + for (let zone of config.zones.features) { + let found = false; + for (let key of dropoffKeys) { + if (zone.properties.keys[key]) found = true; + continue; + } + + if (found) { + dropoffZoneMatches.push(zone.properties.id); + } } - } - if (zoneMatches.length) { - trip.matches.dropoffZones = dropoffZoneMatches; + if (zoneMatches.length) { + trip.matches.dropoffZones = dropoffZoneMatches; + } } - } - return trip; + return trip; + } }; diff --git a/src/providers/local.js b/src/providers/local.js index d161a61..18238d3 100644 --- a/src/providers/local.js +++ b/src/providers/local.js @@ -33,12 +33,14 @@ async function trips( if (trip.start_time >= start && trip.start_time < stop) { trip = await tripMatch(trip, config, graph); - const signature = crypto - .createHmac("sha256", version) - .update(JSON.stringify(trip)) - .digest("hex"); - fs.appendFileSync(cacheDayProviderLogPath, signature + "\n"); - stream.write(JSON.stringify(trip) + "\n"); + if (trip) { + const signature = crypto + .createHmac("sha256", version) + .update(JSON.stringify(trip)) + .digest("hex"); + fs.appendFileSync(cacheDayProviderLogPath, signature + "\n"); + stream.write(JSON.stringify(trip) + "\n"); + } } } next(); @@ -75,12 +77,14 @@ async function changes( change.event_time = change.event_time; if (change.event_time >= start && change.event_time < stop) { change = await changeMatch(change, config, graph); - const signature = crypto - .createHmac("sha256", version) - .update(JSON.stringify(change)) - .digest("hex"); - fs.appendFileSync(cacheDayProviderLogPath, signature + "\n"); - stream.write(JSON.stringify(change) + "\n"); + if (change) { + const signature = crypto + .createHmac("sha256", version) + .update(JSON.stringify(change)) + .digest("hex"); + fs.appendFileSync(cacheDayProviderLogPath, signature + "\n"); + stream.write(JSON.stringify(change) + "\n"); + } } } next(); diff --git a/src/providers/mds.js b/src/providers/mds.js index 3947efe..ac7ad64 100644 --- a/src/providers/mds.js +++ b/src/providers/mds.js @@ -38,12 +38,14 @@ async function trips( // write any returned trips to stream for (let trip of data.data.trips) { trip = await tripMatch(trip, config, graph); - const signature = crypto - .createHmac("sha256", version) - .update(JSON.stringify(trip)) - .digest("hex"); - fs.appendFileSync(cacheDayProviderLogPath, signature + "\n"); - stream.write(JSON.stringify(trip) + "\n"); + if (trip) { + const signature = crypto + .createHmac("sha256", version) + .update(JSON.stringify(trip)) + .digest("hex"); + fs.appendFileSync(cacheDayProviderLogPath, signature + "\n"); + stream.write(JSON.stringify(trip) + "\n"); + } } // continue scan if another page is present @@ -95,12 +97,14 @@ async function changes( // write any returned changes to stream for (let change of data.data.status_changes) { change = await changeMatch(change, config, graph); - const signature = crypto - .createHmac("sha256", version) - .update(JSON.stringify(change)) - .digest("hex"); - fs.appendFileSync(cacheDayProviderLogPath, signature + "\n"); - stream.write(JSON.stringify(change) + "\n"); + if (change) { + const signature = crypto + .createHmac("sha256", version) + .update(JSON.stringify(change)) + .digest("hex"); + fs.appendFileSync(cacheDayProviderLogPath, signature + "\n"); + stream.write(JSON.stringify(change) + "\n"); + } } // continue scan if another page is present