howsmydriving-seattle
Version:
Seattle region plug-in for @HowsMyDrivingWA.
563 lines (562 loc) • 31.3 kB
JavaScript
;
// TODO: Order things in this file
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
var soap_1 = require("soap");
var RestClient = require("typed-rest-client/RestClient");
var moment = require('moment');
var howsmydriving_utils_1 = require("howsmydriving-utils");
var seattleCollision_1 = require("./seattleCollision");
var factory_1 = require("./factory");
exports.__REGION_NAME__ = 'Seattle';
var logging_1 = require("./logging");
// TODO: Consolidate these.
var parkingAndCameraViolationsText = 'Total __REGION__ parking and camera violations for #__LICENSE__: __COUNT__', violationsByYearText = 'Violations by year for #', violationsByStatusText = 'Violations by status for #', citationQueryText = 'License #__LICENSE__ has been queried __COUNT__ times.';
// The Seattle court web service to query citations.
// This could break at any time since they don't document its availability.
var url = 'https://web6.seattle.gov/Courts/ECFPortal/JSONServices/ECFControlsService.asmx?wsdl';
// Classes
var SeattleRegion = /** @class */ (function (_super) {
__extends(SeattleRegion, _super);
function SeattleRegion(state_store) {
var _this = _super.call(this, exports.__REGION_NAME__, state_store) || this;
_this.collision_types = {
fatality: {
last_tweet_date: 0,
tweet_frequency_days: 7
},
'serious injury': {
last_tweet_date: 0,
tweet_frequency_days: 7
},
injury: {
last_tweet_date: 0,
tweet_frequency_days: 7
}
};
logging_1.log.debug("Creating instance of " + _this.name + " for region " + exports.__REGION_NAME__);
_this.initialize_promise = _this.InitializeCollisionInfo();
return _this;
}
SeattleRegion.prototype.InitializeCollisionInfo = function () {
var _this = this;
return new Promise(function (resolve, reject) {
var state_store_promises = [];
logging_1.log.info("Getting last tweet date for collision types for " + exports.__REGION_NAME__ + "...");
Object.keys(_this.collision_types).forEach(function (collision_type) { return __awaiter(_this, void 0, void 0, function () {
var key, value;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
key = "last_" + collision_type + "_tweet_date";
logging_1.log.trace("Getting key " + key + "...");
return [4 /*yield*/, this.state_store.GetStateValueAsync(key)];
case 1:
value = _a.sent();
logging_1.log.trace("Key " + key + " has value " + value + ".");
this.collision_types[collision_type].last_tweet_date = parseInt(value);
return [2 /*return*/];
}
});
}); });
logging_1.log.info("Initialization completed.");
resolve(_this);
});
};
SeattleRegion.prototype.GetCitationsByPlate = function (plate, state) {
var _this = this;
logging_1.log.debug("Getting citations for " + state + ":" + plate + " in " + exports.__REGION_NAME__ + " region.");
return new Promise(function (resolve, reject) {
var citations = [];
logging_1.log.debug("Getting vehicles for " + state + ":" + plate + " in " + exports.__REGION_NAME__ + " region.");
_this.GetVehicleIDs(plate, state).then(function (vehicles) { return __awaiter(_this, void 0, void 0, function () {
var citationsByCitationID, i, vehicle, citations_1, cases, allCitations;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
citationsByCitationID = {};
i = 0;
_a.label = 1;
case 1:
if (!(i < vehicles.length)) return [3 /*break*/, 5];
vehicle = vehicles[i];
logging_1.log.debug("Getting citations for " + state + ":" + plate + " vehicle " + vehicle.VehicleNumber + " in " + exports.__REGION_NAME__ + " region.");
return [4 /*yield*/, this.GetCitationsByVehicleNum(vehicle.VehicleNumber, plate, state).catch(function (err) {
throw err;
})];
case 2:
citations_1 = _a.sent();
return [4 /*yield*/, this.GetCasesByVehicleNum(vehicle.VehicleNumber).catch(function (err) {
throw err;
})];
case 3:
cases = _a.sent();
logging_1.log.info(howsmydriving_utils_1.DumpObject(cases));
citations_1.forEach(function (citation) {
// use the Citation field as the unique citation_id.
citation.citation_id = citation.Citation;
citationsByCitationID[citation.citation_id] = citation;
});
_a.label = 4;
case 4:
i++;
return [3 /*break*/, 1];
case 5:
logging_1.log.info("Found " + Object.keys(citationsByCitationID).length + " different citations for vehicle " + state + ":" + plate);
allCitations = Object.keys(citationsByCitationID).map(function (v) {
return citationsByCitationID[v];
});
resolve(allCitations);
return [2 /*return*/];
}
});
}); });
});
};
SeattleRegion.prototype.ProcessCitationsForRequest = function (citations, query_count) {
var categorizedCitations = {};
// TODO: Does it work to convert Date's to string for sorting? Might have to use epoch.
var chronologicalCitations = {};
var violationsByYear = {};
var violationsByStatus = {};
if (!citations || Object.keys(citations).length == 0) {
// Should never happen. jurisdictions must return at least a dummy citation
throw new Error('Jurisdiction modules must return at least one citation, a dummy one if there are none.');
}
var license;
for (var i = 0; i < citations.length; i++) {
var citation = citations[i];
var year = 1970;
var violationDate = new Date(Date.now());
// All citations are from the same license
if (license == null) {
license = citation.license;
}
try {
violationDate = new Date(Date.parse(citation.ViolationDate));
}
catch (e) {
// TODO: refactor error handling to a separate file
throw new Error(e);
}
// TODO: Is it possible to have more than 1 citation with exact same time?
// Maybe throw an exception if we ever encounter it...
if (!(violationDate.getTime().toString() in chronologicalCitations)) {
chronologicalCitations[violationDate.getTime().toString()] = new Array();
}
chronologicalCitations[violationDate.getTime().toString()].push(citation);
if (!(citation.Type in categorizedCitations)) {
categorizedCitations[citation.Type] = 0;
}
categorizedCitations[citation.Type]++;
if (!(citation.Status in violationsByStatus)) {
violationsByStatus[citation.Status] = 0;
}
violationsByStatus[citation.Status]++;
year = violationDate.getFullYear();
if (!(year.toString() in violationsByYear)) {
violationsByYear[year.toString()] = 0;
}
violationsByYear[year.toString()]++;
}
var general_summary = parkingAndCameraViolationsText
.replace('__LICENSE__', howsmydriving_utils_1.formatPlate(license))
.replace('__REGION__', exports.__REGION_NAME__)
.replace('__COUNT__', Object.keys(citations).length.toString());
Object.keys(categorizedCitations).forEach(function (key) {
var line = key + ': ' + categorizedCitations[key];
// Max twitter username is 15 characters, plus the @
general_summary += '\n';
general_summary += line;
});
general_summary += '\n\n';
general_summary += citationQueryText
.replace('__LICENSE__', howsmydriving_utils_1.formatPlate(license))
.replace('__COUNT__', query_count.toString());
var detailed_list = '';
var sortedChronoCitationKeys = Object.keys(chronologicalCitations).sort(function (a, b) {
return howsmydriving_utils_1.CompareNumericStrings(a, b); //(a === b) ? 0 : ( a < b ? -1 : 1);
});
var first = true;
for (var i = 0; i < sortedChronoCitationKeys.length; i++) {
var key = sortedChronoCitationKeys[i];
chronologicalCitations[key].forEach(function (citation) {
if (first != true) {
detailed_list += '\n';
}
first = false;
detailed_list += citation.ViolationDate + ", " + citation.Type + ", " + citation.ViolationLocation + ", " + citation.Status;
});
}
var temporal_summary = violationsByYearText + howsmydriving_utils_1.formatPlate(license) + ':';
Object.keys(violationsByYear).forEach(function (key) {
temporal_summary += '\n';
temporal_summary += key + ": " + violationsByYear[key].toString();
});
var type_summary = violationsByStatusText + howsmydriving_utils_1.formatPlate(license) + ':';
Object.keys(violationsByStatus).forEach(function (key) {
type_summary += '\n';
type_summary += key + ": " + violationsByStatus[key];
});
// Return them in the order they should be rendered.
return [general_summary, detailed_list, type_summary, temporal_summary];
};
// TODO: If we export this class, this method must be moved out
// because there is no way to declare a function private in a class.
SeattleRegion.prototype.GetVehicleIDs = function (plate, state) {
var args = {
Plate: plate,
State: state
};
return new Promise(function (resolve, reject) {
soap_1.createClient(url, function (err, client) {
if (err) {
throw err;
}
// GetVehicleByPlate returns all vehicles with plates that
// start with the specified plate. So we have to filter the
// results.
client.GetVehicleByPlate(args, function (err, result) {
if (err) {
throw err;
}
var vehicle_records = [];
var jsonObj = JSON.parse(result.GetVehicleByPlateResult);
var jsonResultSet = JSON.parse(jsonObj.Data);
for (var i = 0; i < jsonResultSet.length; i++) {
var vehicle = new Vehicle(jsonResultSet[i]);
if (vehicle.Plate == plate) {
vehicle_records.push(vehicle);
}
}
resolve(vehicle_records);
});
});
});
};
SeattleRegion.prototype.GetCitationsByVehicleNum = function (vehicleID, plate, state) {
var args = {
VehicleNumber: vehicleID,
plate: plate,
state: state
};
logging_1.log.debug("Getting citations for vehicle ID: " + vehicleID + ", " + state + ":" + plate + ".");
return new Promise(function (resolve, reject) {
soap_1.createClient(url, function (err, client) {
if (err) {
throw err;
}
client.GetCitationsByVehicleNumber(args, function (err, citations_result) {
if (err) {
throw err;
}
var jsonObj = JSON.parse(citations_result.GetCitationsByVehicleNumberResult);
var jsonResultSet = JSON.parse(jsonObj.Data);
var citations = [];
jsonResultSet.forEach(function (item) {
var citation = item;
// Add in the citation_id and region fields
citation.citation_id = citation.Citation;
citation.region = exports.__REGION_NAME__;
citations.push(citation);
});
resolve(citations);
});
});
});
};
// TODO: Implement and test this.
SeattleRegion.prototype.GetCasesByVehicleNum = function (vehicleID) {
var args = {
VehicleNumber: vehicleID
};
return new Promise(function (resolve, reject) {
soap_1.createClient(url, function (err, client) {
client.GetCasesByVehicleNumber(args, function (err, cases_result) {
// TODO: This is not right. Need JSON.parse twice.
var cases = JSON.parse(cases_result.GetCasesByVehicleNumberResult);
resolve(cases);
});
});
});
};
SeattleRegion.prototype.GetRecentCollisions = function () {
var _this = this;
return new Promise(function (resolve, reject) {
logging_1.log.info("Getting recent " + _this.name + " collisions...");
_this.initialize_promise
.then(function () {
Promise.all([
_this.getLastCollisionsWithCondition('FATALITIES>0', 1),
_this.getLastCollisionsWithCondition('SERIOUSINJURIES>0', 1),
_this.getLastCollisionsWithCondition('INJURIES>0', 1)
])
.then(function (collisions) {
logging_1.log.info("Returning " + collisions.length + " collisions.");
resolve(collisions);
})
.catch(function (err) {
reject(new Error("Failed to get recent collisions: " + howsmydriving_utils_1.DumpObject(err)));
});
})
.catch(function (err) {
reject(new Error("Initialization of collision info failed: " + howsmydriving_utils_1.DumpObject(err)));
});
});
};
SeattleRegion.prototype.ProcessCollisions = function (collisions) {
return this.processCollisionsForTweets(collisions);
};
SeattleRegion.prototype.getLastCollisionsWithCondition = function (condition, count) {
if (count === void 0) { count = 1; }
return new Promise(function (resolve, reject) {
var restc = new RestClient.RestClient('SDOT-Crashes');
var url = "https://gisdata.seattle.gov/server/rest/services/SDOT/SDOT_Collisions/MapServer/0/query?where=" + condition + "&outFields=*&outSR=4326&f=json&orderByFields=INCDATE DESC&resultRecordCount=" + count;
logging_1.log.trace("Making REST call: " + url);
var resp = restc.get(url);
resp
.then(function (response) {
try {
var id = response['result']['features'][0]['attributes']['INCKEY'] + "-" + response['result']['features'][0]['attributes']['COLDETKEY'];
logging_1.log.debug("getLastCollisionsWithCondition: Creating collision record with id " + id + "...");
var collision = new seattleCollision_1.SeattleCollision({
id: id,
x: response['result']['features'][0]['geometry']['x'],
y: response['result']['features'][0]['geometry']['y'],
date_time: response['result']['features'][0]['attributes']['INCDATE'],
date_time_str: response['result']['features'][0]['attributes']['INCDTTM'],
location: response['result']['features'][0]['attributes']['LOCATION'],
ped_count: response['result']['features'][0]['attributes']['PEDCOUNT'],
cycler_count: response['result']['features'][0]['attributes']['PEDCYLCOUNT'],
person_count: response['result']['features'][0]['attributes']['PERSONCOUNT'],
vehicle_count: response['result']['features'][0]['attributes']['VEHCOUNT'],
injury_count: response['result']['features'][0]['attributes']['INJURIES'],
serious_injury_count: response['result']['features'][0]['attributes']['SERIOUSINJURIES'],
fatality_count: response['result']['features'][0]['attributes']['FATALITIES'],
// TODO: What does a DUI crash look like?
dui: response['result']['features'][0]['attributes']['UNDERINFL'] ===
'Y'
});
Object.assign(collision, response['result']['features'][0]['attributes']);
resolve(collision);
}
catch (err) {
logging_1.log.error("Error: " + howsmydriving_utils_1.DumpObject(err) + ".");
reject(err);
}
})
.catch(function (err) {
logging_1.log.error("Error: " + howsmydriving_utils_1.DumpObject(err) + ".");
reject(err);
});
});
};
SeattleRegion.prototype.processCollisionsForTweets = function (collisions) {
return __awaiter(this, void 0, void 0, function () {
var _this = this;
return __generator(this, function (_a) {
return [2 /*return*/, new Promise(function (resolve, reject) {
var tweets = [];
var latest_collisions = {
fatality: {
latest_collision: undefined
},
'serious injury': {
latest_collision: undefined
},
injury: {
latest_collision: undefined
}
};
_this.initialize_promise
.then(function () {
var now = Date.now();
var date_now = new Date();
collisions.forEach(function (collision) {
var seattle_collision = new seattleCollision_1.SeattleCollision(collision);
var collision_type = SeattleRegion.getCollisionType(collision);
logging_1.log.info("Processing collision " + seattle_collision.id);
logging_1.log.info("About to check date_time. " + collision_type + " last tweet date: " + _this.collision_types[collision_type].last_tweet_date + " now: " + now + " difference is " + moment(date_now).diff(moment(_this.collision_types[collision_type].last_tweet_date), 'days') + " days, tweet every " + _this.collision_types[collision_type].tweet_frequency_days + " days...");
if (!latest_collisions[collision_type].latest_collision ||
latest_collisions[collision_type].latest_collision.date_time <
collision.date_time) {
logging_1.log.debug("Most recent " + collision_type + " found so far is collision " + seattle_collision.id + " on " + seattle_collision.date_time_str + ".");
latest_collisions[collision_type].latest_collision = seattle_collision;
}
});
// Fatalities are serious injuries which are injuries.
if (!latest_collisions['serious injury'].latest_collision) {
latest_collisions['serious injury'].latest_collision =
latest_collisions['fatality'].latest_collision;
}
if (!latest_collisions['injury'].latest_collision) {
latest_collisions['injury'].latest_collision =
latest_collisions['serious injury'].latest_collision;
}
logging_1.log.debug("Checking to see if we will tweet anything...");
// Tweet last fatal collision once per month and
// whenever there is a new one.
var store_updates = {};
Object.keys(_this.collision_types).forEach(function (collision_type) {
logging_1.log.debug("Checking for " + collision_type + " collisions...");
if (latest_collisions[collision_type].latest_collision &&
_this.shouldTweet(latest_collisions[collision_type].latest_collision)) {
var tweet = _this.getTweetFromCollision(latest_collisions[collision_type]
.latest_collision, collision_type, _this.collision_types[collision_type].last_tweet_date);
if (tweet && tweet.length) {
var key = "last_" + collision_type + "_tweet_date";
logging_1.log.debug("Returning tweet " + tweet + ".");
store_updates[key] = now.toString();
tweets.push(tweet);
}
}
else {
logging_1.log.trace("Not tweeting " + collision_type + " last tweet date: " + _this.collision_types[collision_type].last_tweet_date + " now: " + now + " difference is " + moment(date_now).diff(moment(_this.collision_types[collision_type].last_tweet_date), 'days') + " days, tweet every " + _this.collision_types[collision_type].tweet_frequency_days + " days...");
}
});
if (Object.keys(store_updates).length > 0) {
logging_1.log.trace("Writing state values:\n" + howsmydriving_utils_1.DumpObject(store_updates));
_this.state_store.PutStateValues(store_updates).then(function () {
logging_1.log.trace("Returning tweets: " + howsmydriving_utils_1.DumpObject(tweets));
resolve(tweets);
});
}
else {
logging_1.log.trace("No store updates to make.");
resolve(tweets);
}
})
.catch(function (err) {
throw new Error("Initialization of collision info failed: " + howsmydriving_utils_1.DumpObject(err));
});
})];
});
});
};
SeattleRegion.getCollisionType = function (collision) {
var type;
if (collision.fatality_count > 0) {
type = 'fatality';
}
else if (collision.serious_injury_count > 0) {
type = 'serious injury';
}
else if (collision.injury_count > 0) {
type = 'injury';
}
else {
throw new Error("Invalid collision record found: " + howsmydriving_utils_1.DumpObject(collision));
}
return type;
};
SeattleRegion.prototype.getTweetFromCollision = function (collision, collision_type, last_tweeted) {
var tweet = undefined;
var nowDate = new Date();
var last_tweeted_date = new Date(last_tweeted);
if (this.shouldTweet(collision)) {
logging_1.log.debug("Tweeting last " + collision_type + " collision from " + collision.date_time_str + ".");
var media_item = new howsmydriving_utils_1.MediaItem({
url: "https://maps.googleapis.com/maps/api/staticmap?markers=" + collision.y + "," + collision.x + "&zoom=14&size=400x400&key=AIzaSyCK7loPQ04_Ec3uPZIHTPLuTdz1kYU1_xk",
alt_text: "Map with pin on " + collision.location
});
tweet = collision.toString() + "||" + JSON.stringify([media_item]);
}
else {
logging_1.log.debug("Not tweeting last " + collision_type + " collision.");
}
return tweet;
};
SeattleRegion.prototype.shouldTweet = function (collision) {
var collision_type = SeattleRegion.getCollisionType(collision);
if (collision && this.collision_types[collision_type]) {
var days_diff = moment(Date.now()).diff(moment(this.collision_types[collision_type].last_tweet_date), 'days');
logging_1.log.debug("Checking date diff... difference is " + days_diff + " days and tweet frequency is " + this.collision_types[collision_type].tweet_frequency_days + " days.");
if (days_diff >= this.collision_types[collision_type].tweet_frequency_days) {
logging_1.log.trace("Will tweet last " + collision_type + " collision from " + collision.date_time_str + ".");
return true;
}
else {
logging_1.log.trace("Will not tweet last " + collision_type + " collision from " + collision.date_time_str + ".");
}
}
else {
logging_1.log.error("shouldTweet called with collision " + collision + " and last tweet date " + this.collision_types[collision_type].last_tweet_date);
}
return false;
};
return SeattleRegion;
}(howsmydriving_utils_1.Region));
exports.SeattleRegion = SeattleRegion;
var Vehicle = /** @class */ (function () {
function Vehicle(veh) {
this.VehicleNumber = veh.VehicleNumber;
this.Make = veh.Make;
this.Model = veh.Model;
this.Year = veh.Year;
this.State = veh.State;
(this.Plate = veh.Plate), (this.ExpirationYear = veh.ExpirationYear);
this.Color = veh.Color;
this.Style = veh.Style;
this.Dealer = veh.Dealer;
this.VIN = veh.VIN;
this.PlateType = veh.PlateType;
this.DOLReceivedDate = veh.DOLReceivedDate;
this.DOLRequestDate = veh.DOLRequestDate;
}
return Vehicle;
}());
// Print out subset of citation object properties.
function printCitation(citation) {
return "Citation: " + citation.id + ", " + citation.Citation + ", Type: " + citation.Type + ", Status: " + citation.Status + ", Date: " + citation.ViolationDate + ", Location: " + citation.ViolationLocation + ".";
}
var Factory = new factory_1.SeattleRegionFactory();
exports.default = Factory;
exports.Factory = Factory;