Greasy Fork is available in English.

WME State DOT Reports

Display state transportation department reports in WME.

  1. // ==UserScript==
  2. // @name WME State DOT Reports
  3. // @namespace https://greasyfork.org/users/45389
  4. // @version 2020.11.02.002
  5. // @description Display state transportation department reports in WME.
  6. // @author MapOMatic
  7. // @license GNU GPLv3
  8. // @contributionURL https://github.com/WazeDev/Thank-The-Authors
  9. // @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/
  10. // @require https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
  11. // @grant GM_xmlhttpRequest
  12. // @connect indot.carsprogram.org
  13. // @connect hb.511ia.org
  14. // @connect ohgo.com
  15. // @connect hb.511.nebraska.gov
  16. // @connect hb.511.idaho.gov
  17. // @connect hb.511mn.org
  18. // ==/UserScript==
  19.  
  20. /* global $ */
  21. /* global OpenLayers */
  22. /* global GM_info */
  23. /* global W */
  24. /* global unsafeWindow */
  25. /* global WazeWrap */
  26. /* global GM_xmlhttpRequest */
  27.  
  28. const SETTINGS_STORE_NAME = 'dot_report_settings';
  29. const ALERT_UPDATE = false;
  30. const SCRIPT_VERSION = GM_info.script.version;
  31. const SCRIPT_VERSION_CHANGES = [
  32. `${GM_info.script.name}\nv${SCRIPT_VERSION}\n\nWhat's New\n------------------------------\n`,
  33. '\n- Added Copy To Clipboard button on report popups.'
  34. ].join('');
  35. const IMAGES_PATH = 'https://raw.githubusercontent.com/WazeDev/WME-State-DOT-Reports/master/images';
  36. const DOT_INFO = {
  37. ID: {
  38. stateName: 'Idaho',
  39. mapType: 'cars',
  40. baseUrl: 'https://hb.511.idaho.gov',
  41. reportUrl: '/#roadReports/eventAlbum/',
  42. reportsFeedUrl: '/tgevents/api/eventReports'
  43. },
  44. IN: {
  45. stateName: 'Indiana',
  46. mapType: 'cars',
  47. baseUrl: 'https://indot.carsprogram.org',
  48. reportUrl: '/#roadReports/eventAlbum/',
  49. reportsFeedUrl: '/tgevents/api/eventReports'
  50. },
  51. IA: {
  52. stateName: 'Iowa',
  53. mapType: 'cars',
  54. baseUrl: 'https://hb.511ia.org',
  55. reportUrl: '/#allReports/eventAlbum/',
  56. reportsFeedUrl: '/tgevents/api/eventReports'
  57. },
  58. MN: {
  59. stateName: 'Minnesota',
  60. mapType: 'cars',
  61. baseUrl: 'https://hb.511mn.org',
  62. reportUrl: '/#roadReports/eventAlbum/',
  63. reportsFeedUrl: '/tgevents/api/eventReports'
  64. },
  65. NE: {
  66. stateName: 'Nebraska',
  67. mapType: 'cars',
  68. baseUrl: 'https://hb.511.nebraska.gov',
  69. reportUrl: '/#roadReports/eventAlbum/',
  70. reportsFeedUrl: '/tgevents/api/eventReports'
  71. }
  72. };
  73. const _columnSortOrder = ['priority', 'beginTime.time', 'eventDescription.descriptionHeader', 'icon.image', 'archived'];
  74. let _reports = [];
  75. let _previousZoom;
  76. let _mapLayer = null;
  77. let _settings = {};
  78.  
  79. function log(message) {
  80. console.log('DOT Reports: ', message);
  81. }
  82.  
  83. function logDebug(message) {
  84. console.debug('DOT Reports:', message);
  85. }
  86. function logError(message) {
  87. console.error('DOT Reports:', message);
  88. }
  89.  
  90. function copyToClipboard(report) {
  91. // create hidden text element, if it doesn't already exist
  92. const targetId = '_hiddenCopyText_';
  93.  
  94. // must use a temporary form element for the selection and copy
  95. let target = document.getElementById(targetId);
  96. if (!target) {
  97. target = document.createElement('textarea');
  98. target.style.position = 'absolute';
  99. target.style.left = '-9999px';
  100. target.style.top = '0';
  101. target.id = targetId;
  102. document.body.appendChild(target);
  103. }
  104. const startTime = new Date(report.beginTime.time);
  105. const lastUpdateTime = new Date(report.updateTime.time);
  106.  
  107. const $content = $('<div>').html(
  108. `${report.eventDescription.descriptionHeader}<br/><br/>
  109. ${report.eventDescription.descriptionFull}<br/><br/>
  110. Start Time: ${startTime.toString('MMM d, y @ h:mm tt')}<br/>
  111. Updated: ${lastUpdateTime.toString('MMM d, y @ h:mm tt')}`
  112. );
  113.  
  114. $(target).val($content[0].innerText || $content[0].textContent);
  115.  
  116. // select the content
  117. const currentFocus = document.activeElement;
  118. target.focus();
  119. target.setSelectionRange(0, target.value.length);
  120.  
  121. // copy the selection
  122. let succeed = false;
  123. try {
  124. succeed = document.execCommand('copy');
  125. } catch (e) {
  126. // do nothing
  127. }
  128. // restore original focus
  129. if (currentFocus && typeof currentFocus.focus === 'function') {
  130. currentFocus.focus();
  131. }
  132.  
  133. target.textContent = '';
  134. return succeed;
  135. }
  136.  
  137. // I believe this should return the bounds that Waze uses to load its data model.
  138. // It's wider than the visible bounds of the map, to reduce data loading frequency.
  139. function getExpandedDataBounds() {
  140. return W.controller.descartesClient.getExpandedDataBounds(W.map.calculateBounds());
  141. }
  142.  
  143. function createSavableReport(reportIn) {
  144. const attributesToCopy = ['agencyAttribution', 'archived', 'beginTime', 'editorIdentifier', 'eventDescription', 'headlinePhrase',
  145. 'icon', 'id', 'location', 'priority', 'situationUpdateKey', 'starred', 'updateTime'];
  146.  
  147. const reportOut = {};
  148. attributesToCopy.forEach(attr => (reportOut[attr] = reportIn[attr]));
  149.  
  150. return reportOut;
  151. }
  152. function copyToSavableReports(reportsIn) {
  153. const reportsOut = {};
  154. Object.keys(reportsIn).forEach(id => (reportsOut[id] = createSavableReport(reportsIn[id])));
  155. return reportsOut;
  156. }
  157.  
  158. function saveSettingsToStorage() {
  159. if (localStorage) {
  160. const settings = {
  161. lastVersion: SCRIPT_VERSION,
  162. layerVisible: _mapLayer.visibility,
  163. state: _settings.state,
  164. hideArchivedReports: $('#hideDotArchivedReports').is(':checked'),
  165. hideWazeReports: $('#hideDotWazeReports').is(':checked'),
  166. hideNormalReports: $('#hideDotNormalReports').is(':checked'),
  167. hideWeatherReports: $('#hideDotWeatherReports').is(':checked'),
  168. hideCrashReports: $('#hideDotCrashReports').is(':checked'),
  169. hideWarningReports: $('#hideDotWarningReports').is(':checked'),
  170. hideClosureReports: $('#hideDotClosureReports').is(':checked'),
  171. hideRestrictionReports: $('#hideDotRestrictionReports').is(':checked'),
  172. hideFutureReports: $('#hideDotFutureReports').is(':checked'),
  173. hideCurrentReports: $('#hideDotCurrentReports').is(':checked'),
  174. archivedReports: _settings.archivedReports,
  175. starredReports: copyToSavableReports(_settings.starredReports)
  176. };
  177. localStorage.setItem(SETTINGS_STORE_NAME, JSON.stringify(settings));
  178. logDebug('Settings saved');
  179. }
  180. }
  181.  
  182. function dynamicSort(property) {
  183. let sortOrder = 1;
  184. if (property[0] === '-') {
  185. sortOrder = -1;
  186. property = property.substr(1);
  187. }
  188. return (a, b) => {
  189. const props = property.split('.');
  190. props.forEach(prop => {
  191. a = a[prop];
  192. b = b[prop];
  193. });
  194. let result = 0;
  195. if (a < b) {
  196. result = -1;
  197. } else if (a > b) {
  198. result = 1;
  199. }
  200. return result * sortOrder;
  201. };
  202. }
  203.  
  204. function dynamicSortMultiple(...args) {
  205. /*
  206. * save the arguments object as it will be overwritten
  207. * note that arguments object is an array-like object
  208. * consisting of the names of the properties to sort by
  209. */
  210. let props = args;
  211. if (args[0] && Array.isArray(args[0])) {
  212. [props] = args;
  213. }
  214. return (obj1, obj2) => {
  215. let i = 0;
  216. let result = 0;
  217. const numberOfProperties = props.length;
  218. /* try getting a different result from 0 (equal)
  219. * as long as we have extra properties to compare
  220. */
  221. while (result === 0 && i < numberOfProperties) {
  222. result = dynamicSort(props[i])(obj1, obj2);
  223. i++;
  224. }
  225. return result;
  226. };
  227. }
  228.  
  229. function getReport(reportId) {
  230. return _reports.find(report => report.id === reportId);
  231. }
  232.  
  233. function isHideOptionChecked(reportType) {
  234. return $(`#hideDot${reportType}Reports`).is(':checked');
  235. }
  236.  
  237. function updateReportsVisibility() {
  238. hideAllReportPopovers();
  239. const hideArchived = isHideOptionChecked('Archived');
  240. const hideWaze = isHideOptionChecked('Waze');
  241. const hideNormal = isHideOptionChecked('Normal');
  242. const hideWeather = isHideOptionChecked('Weather');
  243. const hideCrash = isHideOptionChecked('Crash');
  244. const hideWarning = isHideOptionChecked('Warning');
  245. const hideRestriction = isHideOptionChecked('Restriction');
  246. const hideClosure = isHideOptionChecked('Closure');
  247. const hideFuture = isHideOptionChecked('Future');
  248. const hideCurrent = isHideOptionChecked('Current');
  249. let visibleCount = 0;
  250. _reports.forEach(report => {
  251. const img = report.icon.image;
  252. const now = Date.now();
  253. const start = new Date(report.beginTime.time);
  254. const hide = (hideArchived && report.archived)
  255. || (hideWaze && img.indexOf('waze') > -1)
  256. || (hideNormal && img.includes('driving'))
  257. || (hideWeather && (img.indexOf('weather') > -1 || img.indexOf('flooding') > -1))
  258. || (hideCrash && img.indexOf('crash') > -1)
  259. || (hideWarning && (img.indexOf('warning') > -1 || img.indexOf('lane_closure') > -1))
  260. || (hideRestriction && img.indexOf('restriction') > -1)
  261. || (hideClosure && img.indexOf('closure') > -1)
  262. || (hideFuture && start > now)
  263. || (hideCurrent && start <= now);
  264. if (hide) {
  265. report.dataRow.hide();
  266. if (report.imageDiv) { report.imageDiv.hide(); }
  267. } else {
  268. visibleCount += 1;
  269. report.dataRow.show();
  270. if (report.imageDiv) { report.imageDiv.show(); }
  271. }
  272. });
  273. $('.dot-report-count').text(`${visibleCount} of ${_reports.length} reports`);
  274. }
  275.  
  276. function hideAllPopovers($excludeDiv) {
  277. _reports.forEach(rpt => {
  278. const $div = rpt.imageDiv;
  279. if ((!$excludeDiv || $div[0] !== $excludeDiv[0]) && $div.data('state') === 'pinned') {
  280. $div.data('state', '');
  281. $div.popover('hide');
  282. }
  283. });
  284. }
  285.  
  286. function deselectAllDataRows() {
  287. _reports.forEach(rpt => rpt.dataRow.css('background-color', 'white'));
  288. }
  289.  
  290. function toggleMarkerPopover($div, forcePin = false) {
  291. hideAllPopovers($div);
  292. if ($div.data('state') !== 'pinned' || forcePin) {
  293. const id = $div.data('reportId');
  294. const report = getReport(id);
  295. $div.data('state', 'pinned');
  296. $div.popover('show');
  297. _mapLayer.setZIndex(100000); // this is to help make sure the report shows on top of the turn restriction arrow layer
  298. if (report.archived) {
  299. $('.btn-archive-dot-report').text('Un-Archive');
  300. }
  301. $('.btn-archive-dot-report').click(() => { setArchiveReport(report, !report.archived, true); buildTable(); });
  302. $('.btn-open-dot-report').click(evt => {
  303. evt.stopPropagation();
  304. window.open($(evt.currentTarget).data('dot-report-url'), '_blank');
  305. });
  306. $('.btn-zoom-dot-report').click(evt => {
  307. evt.stopPropagation();
  308. W.map.setCenter(getReport($(evt.currentTarget).data('dot-report-id')).marker.lonlat);
  309. W.map.olMap.zoomTo(4);
  310. });
  311. $('.btn-copy-dot-report').click(evt => {
  312. evt.stopPropagation();
  313. copyToClipboard(getReport($(evt.currentTarget).data('dot-report-id')));
  314. });
  315. $('.reportPopover,.close-popover').click(evt => {
  316. evt.stopPropagation();
  317. hideAllReportPopovers();
  318. });
  319. // $(".close-popover").click(function() {hideAllReportPopovers();});
  320. $div.data('report').dataRow.css('background-color', 'beige');
  321. } else {
  322. $div.data('state', '');
  323. $div.popover('hide');
  324. }
  325. }
  326.  
  327. function toggleReportPopover($div) {
  328. deselectAllDataRows();
  329. toggleMarkerPopover($div);
  330. }
  331.  
  332. function hideAllReportPopovers() {
  333. deselectAllDataRows();
  334. hideAllPopovers();
  335. }
  336.  
  337. function setArchiveReport(report, archive, updateUi) {
  338. report.archived = archive;
  339. if (archive) {
  340. _settings.archivedReports[report.id] = { updateNumber: report.situationUpdateKey.updateNumber };
  341. report.imageDiv.addClass('dot-archived-marker');
  342. } else {
  343. delete _settings.archivedReports[report.id];
  344. report.imageDiv.removeClass('dot-archived-marker');
  345. }
  346. if (updateUi) {
  347. saveSettingsToStorage();
  348. updateReportsVisibility();
  349. hideAllReportPopovers();
  350. }
  351. }
  352.  
  353. function setStarReport(report, star, updateUi) {
  354. report.starred = star;
  355. if (star) {
  356. if (!_settings.starredReports) { _settings.starredReports = {}; }
  357. _settings.starredReports[report.id] = report;
  358. report.imageDiv.addClass('dot-starred-marker');
  359. } else {
  360. delete _settings.starredReports[report.id];
  361. report.imageDiv.removeClass('dot-starred-marker');
  362. }
  363. if (updateUi) {
  364. saveSettingsToStorage();
  365. updateReportsVisibility();
  366. hideAllReportPopovers();
  367. }
  368. }
  369.  
  370. function archiveAllReports(unarchive) {
  371. _reports.forEach(report => setArchiveReport(report, !unarchive, false));
  372. saveSettingsToStorage();
  373. buildTable();
  374. hideAllReportPopovers();
  375. }
  376.  
  377. function addRow($table, report) {
  378. const $img = $('<img>', { src: report.imgUrl, class: 'table-img' });
  379. const $row = $('<tr> class="clickable"', { id: `dot-row-${report.id}` }).append(
  380. $('<td class="centered">').append(
  381. $('<span>', {
  382. class: `star ${(report.starred ? 'star-filled' : 'star-empty')}`,
  383. title: 'Star if you want notification when this report is removed by the DOT.\nFor instance, if a map change needs to be undone after a closure report is removed.'
  384. }).click(evt => {
  385. evt.stopPropagation();
  386. setStarReport(report, !report.starred, true);
  387. const $target = $(evt.currentTarget);
  388. $target.removeClass(report.starred ? 'star-empty' : 'star-filled');
  389. $target.addClass(report.starred ? 'star-filled' : 'star-empty');
  390. })
  391. ),
  392. $('<td>', { class: 'centered' }).append(
  393. $('<input>', {
  394. type: 'checkbox',
  395. title: 'Archive (will automatically un-archive if report is updated by DOT)',
  396. id: `archive-${report.id}`,
  397. 'data-report-id': report.id
  398. }).prop('checked', report.archived).click(evt => {
  399. evt.stopPropagation();
  400. const $target = $(evt.currentTarget);
  401. const id = $target.data('reportId');
  402. const thisReport = getReport(id);
  403. setArchiveReport(thisReport, $target.is(':checked'), true);
  404. })
  405. ),
  406. $('<td>', { class: 'clickable' }).append($img),
  407. $('<td>', { class: 'centered' }).text(report.priority),
  408. $('<td>', { class: (report.wasRemoved ? 'removed-report' : '') }).text(report.eventDescription.descriptionHeader),
  409. $('<td>', { class: 'centered' }).text(new Date(report.beginTime.time).toString('M/d/y h:mm tt'))
  410. ).click(evt => {
  411. const $thisRow = $(evt.currentTarget);
  412. const id = $thisRow.data('reportId');
  413. const { marker } = getReport(id);
  414. const $imageDiv = report.imageDiv;
  415.  
  416. if ($imageDiv.data('state') !== 'pinned') {
  417. W.map.setCenter(marker.lonlat);
  418. }
  419.  
  420. toggleReportPopover($imageDiv);
  421. }).data('reportId', report.id);
  422. report.dataRow = $row;
  423. $table.append($row);
  424. $row.report = report;
  425. }
  426.  
  427. function onClickColumnHeader(evt) {
  428. const obj = evt.currentTarget;
  429. let prop;
  430. switch (/dot-table-(.*)-header/.exec(obj.id)[1]) {
  431. case 'category':
  432. prop = 'icon.image';
  433. break;
  434. case 'begins':
  435. prop = 'beginTime.time';
  436. break;
  437. case 'desc':
  438. prop = 'eventDescription.descriptionHeader';
  439. break;
  440. case 'priority':
  441. prop = 'priority';
  442. break;
  443. case 'archive':
  444. prop = 'archived';
  445. break;
  446. default:
  447. return;
  448. }
  449. const idx = _columnSortOrder.indexOf(prop);
  450. if (idx > -1) {
  451. _columnSortOrder.splice(idx, 1);
  452. _columnSortOrder.reverse();
  453. _columnSortOrder.push(prop);
  454. _columnSortOrder.reverse();
  455. buildTable();
  456. }
  457. }
  458.  
  459. function buildTable() {
  460. logDebug('Building table');
  461. const $table = $('<table>', { class: 'dot-table' });
  462. $table.append(
  463. $('<thead>').append(
  464. $('<tr>').append(
  465. $('<th>', { id: 'dot-table-star-header', title: 'Favorites' }),
  466. $('<th>', { id: 'dot-table-archive-header', class: 'centered' }).append(
  467. $('<span>', { class: 'fa fa-archive', style: 'font-size:120%', title: 'Sort by archived' })
  468. ),
  469. $('<th>', { id: 'dot-table-category-header', title: 'Sort by report type' }),
  470. $('<th>', { id: 'dot-table-priority-header', title: 'Sort by priority' }).append(
  471. $('<span>', { class: 'fa fa-exclamation-circle', style: 'font-size:120%' })
  472. ),
  473. $('<th>', { id: 'dot-table-desc-header', title: 'Sort by description' }).text('Description'),
  474. $('<th>', { id: 'dot-table-begins-header', title: 'Sort by starting date' }).text('Starts')
  475. )
  476. )
  477. );
  478. _reports.sort(dynamicSortMultiple(_columnSortOrder));
  479. _reports.forEach(report => addRow($table, report));
  480. $('.dot-table').remove();
  481. $('#dot-report-table').append($table);
  482. $('.dot-table th').click(onClickColumnHeader);
  483.  
  484. updateReportsVisibility();
  485. }
  486.  
  487. function getUrgencyString(imagePath) {
  488. const i1 = imagePath.lastIndexOf('_');
  489. const i2 = imagePath.lastIndexOf('.');
  490. return imagePath.substring(i1 + 1, i2);
  491. }
  492.  
  493. function updateReportImageUrl(report) {
  494. const startTime = new Date(report.beginTime.time);
  495. let imgName = report.icon.image;
  496.  
  497. if (imgName.indexOf('flooding') !== -1) {
  498. imgName = imgName.replace('flooding', 'weather').replace('.png', '.gif');
  499. } else if (report.headlinePhrase.category === 5 && report.headlinePhrase.code === 21) {
  500. imgName = '/tg_flooding_urgent.png';
  501. }
  502.  
  503. const now = new Date(Date.now());
  504. if (startTime > now) {
  505. let futureValue;
  506. if (startTime > now.clone().addMonths(2)) {
  507. futureValue = 'pp';
  508. } else if (startTime > now.clone().addMonths(1)) {
  509. futureValue = 'p';
  510. } else {
  511. futureValue = startTime.getDate();
  512. }
  513. imgName = `/tg_future_${futureValue}_${getUrgencyString(imgName)}.gif`;
  514. }
  515. report.imgUrl = IMAGES_PATH + imgName;
  516. }
  517.  
  518. function updateReportGeometry(report) {
  519. const coord = report.location.primaryPoint;
  520. report.location.openLayers = {
  521. primaryPointLonLat: new OpenLayers.LonLat(coord.lon, coord.lat).transform('EPSG:4326', 'EPSG:900913')
  522. };
  523. }
  524.  
  525. function processReport(report) {
  526. if (report.location && report.location.primaryPoint && report.icon) {
  527. const size = new OpenLayers.Size(report.icon.width, report.icon.height);
  528. const icon = new OpenLayers.Icon(report.imgUrl, size, null);
  529. const marker = new OpenLayers.Marker(report.location.openLayers.primaryPointLonLat, icon);
  530. marker.report = report;
  531. // marker.events.register('click', marker, onMarkerClick);
  532. // _mapLayer.addMarker(marker);
  533.  
  534. const dot = DOT_INFO[_settings.state];
  535. const lastUpdateTime = new Date(report.updateTime.time);
  536. const startTime = new Date(report.beginTime.time);
  537. const content = $('<div>').append(
  538. report.eventDescription.descriptionFull,
  539. $('<div>', { style: 'margin-top: 10px;' }).append(
  540. $('<span>', { style: 'font-weight: bold; margin-right: 8px;' }).text('Start Time:'),
  541. startTime.toString('MMM d, y @ h:mm tt'),
  542. ),
  543. $('<div>').append(
  544. $('<span>', { style: 'font-weight: bold; margin-right: 8px;' }).text('Updated:'),
  545. `${lastUpdateTime.toString('MMM d, y @ h:mm tt')}&nbsp;&nbsp;(update #${report.situationUpdateKey.updateNumber})`
  546. ),
  547. $('<div>').append(
  548. $('<hr>', { style: 'margin-bottom: 5px; margin-top: 5px; border-color: gainsboro' }),
  549. $('<div>', { style: 'display: table; width: 100%' }).append(
  550. $('<button>', {
  551. class: 'btn btn-primary, btn-open-dot-report',
  552. style: 'float: left;',
  553. 'data-dot-report-url': dot.baseUrl + dot.reportUrl + report.id
  554. }).text('Open in DOT website'),
  555. $('<button>', {
  556. class: 'btn btn-primary, btn-zoom-dot-report',
  557. style: 'float: left; margin-left: 6px;',
  558. 'data-dot-report-id': report.id
  559. }).text('Zoom'),
  560. $('<button>', {
  561. class: 'btn btn-primary, btn-copy-dot-report',
  562. style: 'float: left; margin-left: 6px;',
  563. 'data-dot-report-id': report.id
  564. }).append('<span class="fa fa-copy">'),
  565. $('<button>', {
  566. class: 'btn btn-primary, btn-archive-dot-report',
  567. style: 'float: right;',
  568. 'data-dot-report-id': report.id
  569. }).text('Archive'),
  570. )
  571. )
  572. ).html();
  573.  
  574. const title = $('<div>', { style: 'width: 100%;' }).append(
  575. $('<div>', { style: 'float: left; max-width: 330px; color: #5989af; font-size: 120%;' }).text(report.eventDescription.descriptionHeader),
  576. $('<div>', { style: 'float: right;' }).append(
  577. // eslint-disable-next-line no-script-url
  578. $('<span>', { class: 'close-popover fa fa-window-close' })
  579. ),
  580. $('<div>', { style: 'clear: both;' })
  581. ).html();
  582.  
  583. const popoverTemplate = $('<div>', { class: 'reportPopover popover', style: 'max-width: 500px; width: 500px;' }).append(
  584. $('<div>', { class: 'arrow' }),
  585. $('<div>', { class: 'popover-title' }),
  586. $('<div>', { class: 'popover-content' })
  587. );
  588.  
  589. const $imageDiv = $(marker.icon.imageDiv)
  590. .css('cursor', 'pointer')
  591. .addClass('dotReport')
  592. .attr({
  593. 'data-toggle': 'popover',
  594. title: '',
  595. 'data-content': content,
  596. 'data-original-title': title
  597. }).popover({
  598. trigger: 'manual',
  599. html: true,
  600. placement: 'auto top',
  601. template: popoverTemplate
  602. }).on('click', () => toggleReportPopover($imageDiv))
  603. .data('reportId', report.id)
  604. .data('state', '')
  605. .data('report', report);
  606.  
  607. if (report.agencyAttribution && report.agencyAttribution.agencyName.toLowerCase().includes('waze')) {
  608. $imageDiv.addClass('wazeReport');
  609. }
  610. if (report.archived) {
  611. $imageDiv.addClass('dot-archived-marker');
  612. }
  613. report.imageDiv = $imageDiv;
  614. report.marker = marker;
  615. }
  616. }
  617.  
  618. function processReports(reports) {
  619. let settingsUpdated = false;
  620. _reports = [];
  621. _mapLayer.clearMarkers();
  622. logDebug('Adding reports to map...');
  623. reports.forEach(report => {
  624. // Exclude pandemic reports (e.g. required social distancing, masks, etc)
  625. const isPandemicReport = report.icon.image.includes('pandemic');
  626. if (!isPandemicReport && report.location && report.location.primaryPoint) {
  627. report.archived = false;
  628. if (_settings.archivedReports.hasOwnProperty(report.id)) {
  629. if (_settings.archivedReports[report.id].updateNumber < report.situationUpdateKey.updateNumber) {
  630. delete _settings.archivedReports[report.id];
  631. } else {
  632. report.archived = true;
  633. }
  634. }
  635. _reports.push(report);
  636. }
  637. });
  638.  
  639. // Check saved starred reports.
  640. Object.keys(_settings.starredReports).forEach(reportId => {
  641. const starredReport = _settings.starredReports[reportId];
  642. const report = getReport(reportId);
  643. if (report) {
  644. report.starred = true;
  645. if (report.situationUpdateKey.updateNumber !== starredReport.situationUpdateKey.updateNumber) {
  646. _settings.starredReports[report.id] = report;
  647. settingsUpdated = true;
  648. }
  649. } else {
  650. // Report has been removed by DOT.
  651. if (!starredReport.wasRemoved) {
  652. starredReport.archived = false;
  653. starredReport.wasRemoved = true;
  654. settingsUpdated = true;
  655. }
  656. _reports.push(starredReport);
  657. }
  658. });
  659. _reports.forEach(report => {
  660. updateReportImageUrl(report);
  661. updateReportGeometry(report);
  662. processReport(report);
  663. });
  664. if (settingsUpdated) {
  665. saveSettingsToStorage();
  666. }
  667. buildTable();
  668. }
  669.  
  670. // This function returns a Promise so that it can be used with async/await.
  671. function makeRequest(url) {
  672. // GM_xmlhttpRequest is necessary to avoid CORS issues on some sites.
  673. return new Promise((resolve, reject) => {
  674. GM_xmlhttpRequest({
  675. method: 'GET',
  676. url,
  677. onload: res => {
  678. if (res.status >= 200 && res.status < 300) {
  679. resolve(res.responseText);
  680. } else {
  681. reject(new Error(`(${this.status}) ${this.statusText}`));
  682. }
  683. },
  684. onerror: res => {
  685. let msg;
  686. if (res.status === 0) {
  687. msg = 'An unknown error occurred while attempting to download DOT data.';
  688. } else {
  689. msg = `Status code ${this.status} - ${this.statusText}`;
  690. }
  691. reject(new Error(msg));
  692. }
  693. });
  694. });
  695. }
  696.  
  697. async function fetchReports() {
  698. const dot = DOT_INFO[_settings.state];
  699. let json;
  700. try {
  701. const url = dot.baseUrl + dot.reportsFeedUrl;
  702. const text = await makeRequest(url);
  703. json = $.parseJSON(text);
  704. } catch (ex) {
  705. logError(new Error(ex.message));
  706. json = [];
  707. }
  708. processReports(json);
  709. }
  710.  
  711. function onLayerVisibilityChanged() {
  712. saveSettingsToStorage();
  713. }
  714.  
  715. /* eslint-disable */
  716. function installIcon() {
  717. OpenLayers.Icon = OpenLayers.Class({
  718. url: null,
  719. size: null,
  720. offset: null,
  721. calculateOffset: null,
  722. imageDiv: null,
  723. px: null,
  724. initialize: function(a, b, c, d){
  725. this.url=a;
  726. this.size=b||{w: 20, h: 20};
  727. this.offset=c||{x: -(this.size.w/2), y: -(this.size.h/2)};
  728. this.calculateOffset=d;
  729. a=OpenLayers.Util.createUniqueID("OL_Icon_");
  730. var div = this.imageDiv=OpenLayers.Util.createAlphaImageDiv(a);
  731. // LEAVE THE FOLLOWING LINE TO PREVENT WME-HARDHATS SCRIPT FROM TURNING ALL ICONS INTO HARDHAT WAZERS --MAPOMATIC
  732. $(div.firstChild).removeClass('olAlphaImg');
  733. },
  734. destroy: function(){ this.erase();OpenLayers.Event.stopObservingElement(this.imageDiv.firstChild);this.imageDiv.innerHTML="";this.imageDiv=null; },
  735. clone: function(){ return new OpenLayers.Icon(this.url, this.size, this.offset, this.calculateOffset); },
  736. setSize: function(a){ null!==a&&(this.size=a); this.draw(); },
  737. setUrl: function(a){ null!==a&&(this.url=a); this.draw(); },
  738. draw: function(a){
  739. OpenLayers.Util.modifyAlphaImageDiv(this.imageDiv, null, null, this.size, this.url, "absolute");
  740. this.moveTo(a);
  741. return this.imageDiv;
  742. },
  743. erase: function(){ null!==this.imageDiv&&null!==this.imageDiv.parentNode&&OpenLayers.Element.remove(this.imageDiv); },
  744. setOpacity: function(a){ OpenLayers.Util.modifyAlphaImageDiv(this.imageDiv, null, null, null, null, null, null, null, a); },
  745. moveTo: function(a){
  746. null!==a&&(this.px=a);
  747. null!==this.imageDiv&&(null===this.px?this.display(!1): (
  748. this.calculateOffset&&(this.offset=this.calculateOffset(this.size)),
  749. OpenLayers.Util.modifyAlphaImageDiv(this.imageDiv, null, {x: this.px.x+this.offset.x, y: this.px.y+this.offset.y})
  750. ));
  751. },
  752. display: function(a){ this.imageDiv.style.display=a?"": "none"; },
  753. isDrawn: function(){ return this.imageDiv&&this.imageDiv.parentNode&&11!=this.imageDiv.parentNode.nodeType; },
  754. CLASS_NAME: "OpenLayers.Icon"
  755. });
  756. }
  757. /* eslint-enable */
  758.  
  759. function onStateSelectChange(evt) {
  760. hideAllReportPopovers();
  761. _settings.state = evt.currentTarget.value;
  762. saveSettingsToStorage();
  763. fetchReports();
  764. }
  765.  
  766. function onHideReportTypeCheckChange() {
  767. saveSettingsToStorage();
  768. updateReportsVisibility();
  769. }
  770.  
  771. function isLoading() {
  772. return $('.dot-refresh-reports').hasClass('fa-spin');
  773. }
  774. function beforeLoading() {
  775. const spinner = $('.dot-refresh-reports');
  776. spinner.addClass('fa-spin').css({ cursor: 'auto' });
  777. hideAllReportPopovers();
  778. }
  779. function afterLoading() {
  780. const spinner = $('.dot-refresh-reports');
  781. spinner.removeClass('fa-spin').css({ cursor: 'pointer' });
  782. WazeWrap.Alerts.success(null, 'DOT reports refreshed');
  783. }
  784.  
  785. async function onRefreshReportsClick(evt) {
  786. evt.stopPropagation();
  787. if (!isLoading()) {
  788. beforeLoading();
  789. await fetchReports();
  790. afterLoading();
  791. }
  792. }
  793.  
  794. function init511ReportsOverlay() {
  795. installIcon();
  796. _mapLayer = new OpenLayers.Layer.Markers('State DOT Reports', {
  797. displayInLayerSwitcher: true,
  798. uniqueName: '__stateDotReports'
  799. });
  800.  
  801. W.map.addLayer(_mapLayer);
  802. _mapLayer.setVisibility(_settings.layerVisible);
  803. _mapLayer.setZIndex(100000);
  804. _mapLayer.events.register('visibilitychanged', null, onLayerVisibilityChanged);
  805. }
  806.  
  807. function initSideTab() {
  808. $('#stateDotStateSelect').change(onStateSelectChange);
  809. $('[id^=hideDot]').change(onHideReportTypeCheckChange);
  810. $('#stateDotStateSelect').val(_settings.state);
  811.  
  812. ['ArchivedReports', 'WazeReports', 'NormalReports', 'WeatherReports',
  813. 'TrafficReports', 'CrashReports', 'WarningReports', 'RestrictionReports',
  814. 'ClosureReports', 'FutureReports', 'CurrentReports'].forEach(name => {
  815. const settingsPropName = `hide${name}`;
  816. const checkboxId = `hideDot${name}`;
  817. if (_settings[settingsPropName]) {
  818. $(`#${checkboxId}`).prop('checked', true);
  819. }
  820. });
  821.  
  822. $('<span>', {
  823. title: 'Click to refresh DOT reports',
  824. class: 'fa fa-refresh refreshIcon dot-tab-icon dot-refresh-reports',
  825. style: 'cursor:pointer;'
  826. }).appendTo($('a[href="#sidepanel-dot"]'));
  827.  
  828. $('.dot-refresh-reports').click(onRefreshReportsClick);
  829. }
  830.  
  831. function buildSideTab() {
  832. // Helper template functions to create elements
  833. const createCheckbox = (id, text) => $('<div>', { class: 'controls-container' }).append(
  834. $('<input>', { type: 'checkbox', id }),
  835. $('<label>', { for: id }).text(text)
  836. );
  837. const createOption = (value, text) => $('<option>', { value }).text(text);
  838.  
  839. const panel = $('<div>').append(
  840. $('<div>', { class: 'side-panel-section>' }).append(
  841. $('<div>', { class: 'form-group' }).append(
  842. $('<label>', { class: 'control-label' }).text('Select your state'),
  843. $('<div>', { class: 'controls', id: 'state-select' }).append(
  844. $('<div>').append(
  845. $('<select>', { id: 'stateDotStateSelect', class: 'form-control' }).append(
  846. Object.keys(DOT_INFO).map(abbr => createOption(abbr, DOT_INFO[abbr].stateName))
  847. )
  848. )
  849. ),
  850. $('<label style="width:100%; cursor:pointer; border-bottom: 1px solid #e0e0e0; margin-top:9px;" data-toggle="collapse" data-target="#dotSettingsCollapse"><span class="fa fa-caret-down" style="margin-right:5px;font-size:120%;"></span>Hide reports...</label>'),
  851. $('<div>', { id: 'dotSettingsCollapse', class: 'collapse' }).append(
  852. createCheckbox('hideDotArchivedReports', 'Archived'),
  853. createCheckbox('hideDotWazeReports', 'Waze (if supported by DOT)'),
  854. createCheckbox('hideDotNormalReports', 'Driving conditions'),
  855. createCheckbox('hideDotWeatherReports', 'Weather'),
  856. createCheckbox('hideDotCrashReports', 'Crash'),
  857. createCheckbox('hideDotWarningReports', 'Warning'),
  858. createCheckbox('hideDotRestrictionReports', 'Restriction'),
  859. createCheckbox('hideDotClosureReports', 'Closure'),
  860. createCheckbox('hideDotFutureReports', 'Future'),
  861. createCheckbox('hideDotCurrentReports', 'Current/Past')
  862. )
  863. )
  864. ),
  865. $('<div>', { class: 'side-panel-section>', id: 'dot-report-table' }).append(
  866. $('<div>').append(
  867. $('<span>', {
  868. title: 'Click to refresh DOT reports',
  869. class: 'fa fa-refresh refreshIcon dot-refresh-reports dot-table-label',
  870. style: 'cursor:pointer;'
  871. }),
  872. $('<span>', { class: 'dot-table-label dot-report-count count' }),
  873. $('<span>', { class: 'dot-table-label dot-table-action right' }).text('Archive all').click(() => {
  874. if (confirm(`Archive all reports for ${_settings.state}?`)) {
  875. archiveAllReports(false);
  876. }
  877. }),
  878. $('<span>', { class: 'dot-table-label right' }).text('|'),
  879. $('<span>', { class: 'dot-table-label dot-table-action right' }).text('Un-Archive all').click(() => {
  880. if (confirm(`Un-archive all reports for ${_settings.state}?`)) {
  881. archiveAllReports(true);
  882. }
  883. })
  884. )
  885. )
  886. );
  887.  
  888. new WazeWrap.Interface.Tab('DOT', panel.html(), initSideTab, null);
  889. }
  890.  
  891. function showScriptInfoAlert() {
  892. /* Check version and alert on update */
  893. if (ALERT_UPDATE && SCRIPT_VERSION !== _settings.lastVersion) {
  894. alert(SCRIPT_VERSION_CHANGES);
  895. }
  896. }
  897.  
  898. function initGui() {
  899. init511ReportsOverlay();
  900. buildSideTab();
  901. showScriptInfoAlert();
  902.  
  903. $(`<style type="text/css">
  904. .dot-table th,td,tr {cursor: default;}
  905. .dot-table .centered {text-align:center;}
  906. .dot-table th:hover,tr:hover {background-color: aliceblue;outline: -webkit-focus-ring-color auto 5px;}
  907. .dot-table th:hover {color: blue;border-color: whitesmoke; }
  908. .dot-table {border: 1px solid gray;border-collapse: collapse;width: 100%;font-size: 83%;margin: 0px 0px 0px 0px}
  909. .dot-table th,td {border: 1px solid gainsboro;}
  910. .dot-table td,th {color: black;padding: 1px 4px;}
  911. .dot-table th {background-color: gainsboro;}
  912. .dot-table .table-img {max-width: 24px;max-height: 24px;}
  913. .tooltip.top > .tooltip-arrow {border-top-color: white;}
  914. .tooltip.bottom > .tooltip-arrow {border-bottom-color: white;}
  915. .close-popover { cursor: pointer;font-size: 20px; }
  916. .close-popover:hover { color: #f35252; }
  917. .refreshIcon:hover {color:blue;text-shadow: 2px 2px #aaa;}
  918. .refreshIcon:active { text-shadow: 0px 0px; }
  919. .dot-tab-icon { margin-left: 10px; }
  920. .dot-archived-marker {opacity: 0.5;}
  921. .dot-table-label {font-size: 85%;}
  922. .dot-table-action:hover {color: blue;cursor: pointer}
  923. .dot-table-label.right {float: right}
  924. .dot-table-label.count {margin-left: 4px;}
  925. .dot-table .star {cursor: pointer;width: 18px;height: 18px;margin-top: 3px;}
  926. .dot-table .star-empty {content: url(${IMAGES_PATH}/star-empty.png);}
  927. .dot-table .star-filled {content: url(${IMAGES_PATH}/star-filled.png);}
  928. .dot-table .removed-report {text-decoration: line-through;color: #bbb}
  929. </style>`).appendTo('head');
  930.  
  931. _previousZoom = W.map.zoom;
  932. W.map.events.register('zoomend', null, () => {
  933. if (_previousZoom !== W.map.zoom) {
  934. hideAllReportPopovers();
  935. }
  936. _previousZoom = W.map.zoom;
  937. });
  938. }
  939.  
  940. function loadSettingsFromStorage() {
  941. let settings = $.parseJSON(localStorage.getItem(SETTINGS_STORE_NAME));
  942. if (!settings) {
  943. settings = {
  944. lastVersion: null,
  945. layerVisible: true,
  946. state: 'ID',
  947. hideArchivedReports: true,
  948. archivedReports: {}
  949. };
  950. } else {
  951. settings.layerVisible = (settings.layerVisible === true);
  952. settings.state = settings.state ? settings.state : Object.keys(DOT_INFO)[0];
  953. if (typeof settings.hideArchivedReports === 'undefined') {
  954. settings.hideArchivedReports = true;
  955. }
  956. settings.archivedReports = settings.archivedReports ? settings.archivedReports : {};
  957. settings.starredReports = settings.starredReports ? settings.starredReports : {};
  958. }
  959. _settings = settings;
  960. }
  961.  
  962. function addMarkers() {
  963. _mapLayer.clearMarkers();
  964. const dataBounds = getExpandedDataBounds();
  965. _reports.forEach(report => {
  966. if (dataBounds.containsLonLat(report.location.openLayers.primaryPointLonLat)) {
  967. _mapLayer.addMarker(report.marker);
  968. }
  969. });
  970. }
  971.  
  972. function onMoveEnd() {
  973. addMarkers();
  974. }
  975.  
  976. async function init() {
  977. loadSettingsFromStorage();
  978. W.map.events.register('moveend', null, onMoveEnd);
  979. unsafeWindow.addEventListener('beforeunload', saveSettingsToStorage, false);
  980. initGui();
  981. await fetchReports();
  982. addMarkers();
  983. log('Initialized');
  984. }
  985.  
  986. function bootstrap() {
  987. if (W && W.loginManager
  988. && W.loginManager.events.register
  989. && W.map && W.loginManager.user
  990. && WazeWrap.Ready) {
  991. log('Initializing...');
  992. init();
  993. } else {
  994. log('Bootstrap failed. Trying again...');
  995. setTimeout(bootstrap, 1000);
  996. }
  997. }
  998.  
  999. bootstrap();