UNPKG

howsmydriving-seattle

Version:
563 lines (562 loc) 31.3 kB
"use strict"; // 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;