/** * System intel module */ define([ 'jquery', 'app/init', 'app/util', 'bootbox', 'app/counter', 'app/map/util', ], ($, Init, Util, bootbox, Counter, MapUtil) => { 'use strict'; let config = { // module info modulePosition: 1, moduleName: 'systemIntel', moduleHeadClass: 'pf-module-head', // class for module header moduleHandlerClass: 'pf-module-handler-drag', // class for "drag" handler // system intel module moduleTypeClass: 'pf-system-intel-module', // class for this module // headline toolbar moduleHeadlineIconClass: 'pf-module-icon-button', // class for toolbar icons in the head moduleHeadlineIconAddClass: 'pf-module-icon-button-add', // class for "add structure" icon moduleHeadlineIconReaderClass: 'pf-module-icon-button-reader', // class for "dScan reader" icon moduleHeadlineIconRefreshClass: 'pf-module-icon-button-refresh', // class for "refresh" icon // system intel module intelTableId: 'pf-intel-table-', // id prefix for all tables in module intelTableRowIdPrefix: 'pf-intel-row-', // id prefix for table rows systemStationsTableClass: 'pf-system-station-table', // class for NPC owned stations table systemStructuresTableClass: 'pf-system-structure-table', // class for player owned structures table // structure dialog structureDialogId: 'pf-structure-dialog', // id for "structure" dialog nameInputId: 'pf-structure-dialog-name-input', // id for "name" input statusSelectId: 'pf-structure-dialog-status-select', // id for "status" select typeSelectId: 'pf-structure-dialog-type-select', // id for "type" select corporationSelectId: 'pf-structure-dialog-corporation-select', // id for "corporation" select descriptionTextareaId: 'pf-structure-dialog-description-textarea', // id for "description" textarea descriptionTextareaCharCounter: 'pf-form-field-char-count', // class for "character counter" element for form field // dataTable tableCellImageClass: 'pf-table-image-smaller-cell', // class for table "image" cells tableCellCounterClass: 'pf-table-counter-cell', // class for table "counter" cells tableCellEllipsisClass: 'pf-table-cell-ellipses-auto', // class for table "ellipsis" cells tableCellActionClass: 'pf-table-action-cell', // class for "action" cells tableCellActionIconClass: 'pf-table-action-icon-cell', // class for table "action" icon (icon is part of cell content) tableCellServicesClass: 'pf-table-services-cell' // class for table station "services" cells }; let maxDescriptionLength = 512; /** * get status icon for structure * @param statusData * @returns {string} */ let getIconForStatusData = statusData => { return ''; }; /** * get icon that marks a table cell as clickable * @returns {string} */ let getIconForInformationWindow = () => { return ''; }; /** * get dataTable id * @param mapId * @param systemId * @param tableType * @returns {string} */ let getTableId = (tableType, mapId, systemId) => Util.getTableId(config.intelTableId, tableType, mapId, systemId); /** * get dataTable row id * @param tableType * @param id * @returns {string} */ let getRowId = (tableType, id) => Util.getTableRowId(config.intelTableRowIdPrefix, tableType, id); /** * get DOM id by id * @param tableApi * @param id * @returns {*} */ let getRowById = (tableApi, id) => { return tableApi.rows().ids().toArray().find(rowId => rowId === getRowId(Util.getObjVal(getTableMetaData(tableApi), 'type'), id)); }; /** * get custom "metaData" from dataTables API * @param tableApi * @returns {*} */ let getTableMetaData = tableApi => { let data = null; if(tableApi){ data = tableApi.init().pfMeta; } return data; }; /** * vormat roman numeric string to int * -> e.g. 'VII' => 7 * @param str * @returns {number} */ let romanToInt = str => { let charToTnt = char => { switch (char) { case 'I': return 1; case 'V': return 5; case 'X': return 10; case 'L': return 50; case 'C': return 100; case 'D': return 500; case 'M': return 1000; default: return -1; } }; if(str == null) return -1; let num = charToTnt(str.charAt(0)); let pre, curr; for(let i = 1; i < str.length; i++){ curr = charToTnt(str.charAt(i)); pre = charToTnt(str.charAt(i - 1)); if(curr <= pre){ num += curr; }else{ num = num - pre * 2 + curr; } } return num; }; /** * callback -> add table rows from grouped tableData * @param context * @param tableData * @param groupedDataKey */ let callbackUpdateTableRows = (context, tableData, groupedDataKey = 'structures') => { let touchedRows = []; let hadData = context.tableApi.rows().any(); let notificationCounter = { added: 0, changed: 0, deleted: 0 }; if(tableData){ for(let [rowGroupId, rowGroupData] of Object.entries(tableData)){ if(rowGroupData[groupedDataKey] && rowGroupData[groupedDataKey].length){ for(let rowData of rowGroupData[groupedDataKey]){ let rowId = getRowById(context.tableApi, rowData.id); // add rowGroupData as well to each rowData rowData.rowGroupData = { id: rowGroupData.id, name: rowGroupData.name, groupedDataKey: groupedDataKey }; if(rowId){ // update row let api = context.tableApi.row('#' + rowId); let rowDataCurrent = api.data(); // check for update if(rowDataCurrent.updated.updated !== rowData.updated.updated){ // row data changed -> update api.data(rowData); notificationCounter.changed++; } touchedRows.push(api.id()); }else{ // insert new row let api = context.tableApi.row.add(rowData); api.nodes().to$().data('animationStatus', 'added'); notificationCounter.added++; touchedRows.push(api.id()); } } } } } if(context.removeMissing){ let api = context.tableApi.rows((idx, data, node) => !touchedRows.includes(node.id)); notificationCounter.deleted += api.ids().count(); api.remove(); } if( notificationCounter.added > 0 || notificationCounter.changed > 0 || notificationCounter.deleted > 0 ){ context.tableApi.draw(); } // show notification ------------------------------------------------------------------------------------------ let notification = ''; notification += notificationCounter.added > 0 ? notificationCounter.added + ' added
' : ''; notification += notificationCounter.changed > 0 ? notificationCounter.changed + ' changed
' : ''; notification += notificationCounter.deleted > 0 ? notificationCounter.deleted + ' deleted
' : ''; if(hadData && notification.length){ Util.showNotify({title: 'Structures updated', text: notification, type: 'success'}); } }; /** * callback -> delete structure rows * @param context * @param structureIds */ let callbackDeleteStructures = (context, structureIds) => { let deletedCounter = 0; if(structureIds && structureIds.length){ for(let structureId of structureIds){ let rowId = getRowById(context.tableApi, structureId); if(rowId){ context.tableApi.row('#' + rowId).remove(); deletedCounter++; } } } if(deletedCounter){ context.tableApi.draw(); Util.showNotify({title: 'Structure deleted', text: deletedCounter + ' deleted', type: 'success'}); } }; /** * send ajax request * @param url * @param requestData * @param context * @param callback */ let sendRequest = (url, requestData, context, callback) => { context.moduleElement.showLoadingAnimation(); $.ajax({ url: url, type: 'POST', dataType: 'json', data: requestData, context: context }).done(function(data){ callback(this, data); }).fail(function(jqXHR, status, error){ let reason = status + ' ' + error; Util.showNotify({title: jqXHR.status + ': System intel data', text: reason, type: 'warning'}); $(document).setProgramStatus('problem'); }).always(function(){ // hide loading animation this.moduleElement.hideLoadingAnimation(); }); }; /** * show structure dialog * @param moduleElement * @param tableApi * @param systemId * @param structureData */ let showStructureDialog = (moduleElement, tableApi, systemId, structureData) => { let structureStatusData = Util.getObjVal(Init, 'structureStatus'); let statusData = Object.keys(structureStatusData).map((k) => { let data = structureStatusData[k]; data.selected = data.id === Util.getObjVal(structureData, 'status.id'); return data; }); // if current user is currently docked at a structure (not station) // -> add a modal button for pre-fill modal with it // -> systemId must match systemId from current character log let currentUserData = Util.getCurrentUserData(); let isCurrentLocation = false; let characterStructureId = Util.getObjVal(currentUserData, 'character.log.structure.id') || 0; let characterStructureName = Util.getObjVal(currentUserData, 'character.log.structure.name') || ''; let characterStructureTypeId = Util.getObjVal(currentUserData, 'character.log.structure.type.id') || 0; let characterStructureTypeName = Util.getObjVal(currentUserData, 'character.log.structure.type.name') || ''; if(systemId === Util.getObjVal(currentUserData, 'character.log.system.id')){ isCurrentLocation = true; } let disableButtonAutoFill = true; let buttonLabelAutoFill = ' '; if(characterStructureId){ buttonLabelAutoFill += characterStructureTypeName + ' "' + characterStructureName + '"'; if(isCurrentLocation){ disableButtonAutoFill = false; } }else{ buttonLabelAutoFill += 'unknown structure'; } let data = { id: config.structureDialogId, structureData: structureData, structureStatus: statusData, nameInputId: config.nameInputId, statusSelectId: config.statusSelectId, typeSelectId: config.typeSelectId, corporationSelectId: config.corporationSelectId, descriptionTextareaId: config.descriptionTextareaId, descriptionTextareaCharCounter: config.descriptionTextareaCharCounter, maxDescriptionLength: maxDescriptionLength }; requirejs(['text!templates/dialog/structure.html', 'mustache'], (template, Mustache) => { let content = Mustache.render(template, data); let structureDialog = bootbox.dialog({ title: 'Structure', message: content, show: false, buttons: { close: { label: 'cancel', className: 'btn-default pull-left' }, autoFill: { label: buttonLabelAutoFill, className: 'btn-primary' + (disableButtonAutoFill ? ' pf-font-italic disabled' : ''), callback: function(){ let form = this.find('form'); form.find('#' + config.nameInputId).val(characterStructureName); form.find('#' + config.statusSelectId).val(2).trigger('change'); form.find('#' + config.typeSelectId).val(characterStructureTypeId).trigger('change'); return false; } }, success: { label: ' save', className: 'btn-success', callback: function(){ let form = this.find('form'); // validate form form.validator('validate'); // check whether the form is valid let formValid = form.isValidForm(); if(formValid){ // get form data let formData = form.getFormValues(); formData.id = Util.getObjVal(structureData, 'id') | 0; formData.structureId = Util.getObjVal(formData, 'structureId') | 0; formData.corporationId = Util.getObjVal(formData, 'corporationId') | 0; formData.systemId = systemId | 0; moduleElement.showLoadingAnimation(); let method = formData.id ? 'PATCH' : 'PUT'; Util.request(method, 'structure', formData.id, formData, { moduleElement: moduleElement, tableApi: tableApi }, context => context.moduleElement.hideLoadingAnimation() ).then( payload => callbackUpdateTableRows(payload.context, payload.data), Util.handleAjaxErrorResponse ); }else{ return false; } } } } }); structureDialog.on('show.bs.modal', function(e){ let modalContent = $('#' + config.structureDialogId); // init type select live search let selectElementType = modalContent.find('#' + config.typeSelectId); selectElementType.initUniverseTypeSelect({ categoryIds: [65], maxSelectionLength: 1, selected: [Util.getObjVal(structureData, 'structure.id')] }); // init corporation select live search let selectElementCorporation = modalContent.find('#' + config.corporationSelectId); selectElementCorporation.initUniverseSearch({ categoryNames: ['corporation'], maxSelectionLength: 1 }); // init status select2 modalContent.find('#' + config.statusSelectId).initStatusSelect({ data: statusData }); // init char counter let textarea = modalContent.find('#' + config.descriptionTextareaId); let charCounter = modalContent.find('.' + config.descriptionTextareaCharCounter); Util.updateCounter(textarea, charCounter, maxDescriptionLength); textarea.on('keyup', function(){ Util.updateCounter($(this), charCounter, maxDescriptionLength); }); // set form validator (after select2 init finish) modalContent.find('form').initFormValidation(); }); // show dialog structureDialog.modal('show'); }); }; /** * show D-Scan reader dialog * @param moduleElement * @param tableApi * @param systemData */ let showDscanReaderDialog = (moduleElement, tableApi, systemData) => { requirejs(['text!templates/dialog/dscan_reader.html', 'mustache'], (template, Mustache) => { let structureDialog = bootbox.dialog({ title: 'D-Scan reader', message: Mustache.render(template, {}), show: true, buttons: { close: { label: 'cancel', className: 'btn-default' }, success: { label: ' update intel', className: 'btn-success', callback: function(){ let form = this.find('form'); let formData = form.getFormValues(); updateStructureTableByClipboard(systemData, formData.clipboard, { moduleElement: moduleElement, tableApi: tableApi }); } } } }); // dialog shown event structureDialog.on('shown.bs.modal', function(e){ // set focus on textarea structureDialog.find('textarea').focus(); }); }); }; /** * init station services tooltips * @param element * @param tableApi */ let initStationServiceTooltip = (element, tableApi) => { element.hoverIntent({ over: function(e){ let cellElement = $(this); let rowData = tableApi.row(cellElement.parents('tr')).data(); cellElement.addStationServiceTooltip(Util.getObjVal(rowData, 'services'), { placement: 'left', trigger: 'manual', show: true }); }, out: function(e){ $(this).destroyPopover(); }, selector: 'td.' + config.tableCellServicesClass }); }; /** * get dataTables default options for intel tables * @returns {*} */ let getDataTableDefaults = () => { return { paging: false, lengthChange: false, ordering: true, info: false, searching: false, hover: false, autoWidth: false, drawCallback: function (settings) { let tableApi = this.api(); let columnCount = tableApi.columns(':visible').count(); let rows = tableApi.rows({page: 'current'}).nodes(); let last = null; tableApi.column('rowGroupData:name', {page: 'current'}).data().each(function (group, i) { if (!last || last.id !== group.id) { // "stations" are grouped by "raceId" with its "factionId" // "structures" are grouped by "corporationId" that ADDED it (not the ingame "owner" of it) let imgType = 'stations' === group.groupedDataKey ? 'alliance' : 'corporation'; $(rows).eq(i).before( '' + '' + '' + '' + '' + '' + group.name + '' + '' ); last = group; } }); let animationRows = rows.to$().filter(function () { return ( $(this).data('animationStatus') || $(this).data('animationTimer') ); }); for (let i = 0; i < animationRows.length; i++) { let animationRow = $(animationRows[i]); animationRow.pulseBackgroundColor(animationRow.data('animationStatus')); animationRow.removeData('animationStatus'); } } }; }; /** * get module element * @param parentElement * @param mapId * @param systemData * @returns {jQuery} */ let getModule = (parentElement, mapId, systemData) => { let showStationTable = ['H', 'L', '0.0', 'C12'].includes(Util.getObjVal(systemData, 'security')); let corporationId = Util.getCurrentUserInfo('corporationId'); let moduleElement = $('
').append( $('
', { class: config.moduleHeadClass }).append( $('
', { class: config.moduleHandlerClass }), $('
', { class: 'pull-right' }).append( $('', { class: ['fas', 'fa-fw', 'fa-plus', config.moduleHeadlineIconClass, config.moduleHeadlineIconAddClass].join(' '), title: 'add' }).attr('data-html', 'true').attr('data-toggle', 'tooltip'), $('', { class: ['fas', 'fa-fw', 'fa-paste', config.moduleHeadlineIconClass, config.moduleHeadlineIconReaderClass].join(' '), title: 'D-Scan reader' }).attr('data-html', 'true').attr('data-toggle', 'tooltip'), $('', { class: ['fas', 'fa-fw', 'fa-sync', config.moduleHeadlineIconClass, config.moduleHeadlineIconRefreshClass].join(' '), title: 'refresh all' }).attr('data-html', 'true').attr('data-toggle', 'tooltip') ), $('
', { text: 'Structures' }) ) ); // "Structures" table ----------------------------------------------------------------------------------------- let structureTable = $('', { id: getTableId('structure', mapId, systemData.id), class: ['compact', 'stripe', 'order-column', 'row-border', 'pf-table-fixed', config.systemStructuresTableClass].join(' ') }); moduleElement.append(structureTable); let structureDataTableOptions = { pfMeta: { type: 'structures' }, order: [[10, 'desc' ], [0, 'asc' ]], rowId: rowData => getRowId('structures', rowData.id), language: { emptyTable: 'No structures recorded', info: '_START_ to _END_ of _MAX_', infoEmpty: '' }, columnDefs: [ { targets: 0, name: 'status', title: '', width: 2, className: ['text-center', 'all'].join(' '), data: 'status', render: { display: data => getIconForStatusData(data), sort: data => data.id }, createdCell: function(cell, cellData, rowData, rowIndex, colIndex){ $(cell).find('i').tooltip(); } },{ targets: 1, name: 'structureImage', title: '', width: 24, orderable: false, className: [config.tableCellImageClass, 'text-center', 'all'].join(' '), data: 'structure.id', defaultContent: '', render: { _: function(data, type, row, meta){ let value = data; if(type === 'display' && value){ value = ''; } return value; } } },{ targets: 2, name: 'structureType', title: 'type', width: 30, className: [config.tableCellEllipsisClass, 'all'].join(' '), data: 'structure.name', defaultContent: '', },{ targets: 3, name: 'name', title: 'name', width: 60, className: [config.tableCellEllipsisClass, 'all'].join(' '), data: 'name' },{ targets: 4, name: 'ownerImage', title: '', width: 24, orderable: false, className: [config.tableCellImageClass, 'text-center', 'all'].join(' '), data: 'owner.id', defaultContent: '', render: { _: function(data, type, row, meta){ let value = data; if(type === 'display' && value){ value = ''; value += ''; value += ''; } return value; } } },{ targets: 5, name: 'ownerName', title: 'owner', width: 50, className: [config.tableCellEllipsisClass, 'all'].join(' '), data: 'owner.name', defaultContent: '', },{ targets: 6, name: 'note', title: 'note', className: [config.tableCellEllipsisClass, 'all'].join(' '), data: 'description' },{ targets: 7, name: 'updated', title: 'updated', width: 60, className: ['text-right', config.tableCellCounterClass, 'not-screen-l'].join(' '), data: 'updated.updated' },{ targets: 8, name: 'edit', title: '', orderable: false, width: 10, className: ['text-center', config.tableCellActionClass, config.moduleHeadlineIconClass, 'all'].join(' '), data: null, render: { display: data => { let icon = ''; if(data.rowGroupData.id !== corporationId){ icon = ''; } return icon; } }, createdCell: function(cell, cellData, rowData, rowIndex, colIndex){ let tableApi = this.api(); if($(cell).is(':empty')){ $(cell).removeClass(config.tableCellActionClass + ' ' + config.moduleHeadlineIconClass); }else{ $(cell).on('click', function(e){ // get current row data (important!) // -> "rowData" param is not current state, values are "on createCell()" state rowData = tableApi.row( $(cell).parents('tr')).data(); showStructureDialog(moduleElement, tableApi, systemData.systemId, rowData); }); } } },{ targets: 9, name: 'delete', title: '', orderable: false, width: 10, className: ['text-center', config.tableCellActionClass, 'all'].join(' '), data: null, render: { display: data => { let icon = ''; if(data.rowGroupData.id !== corporationId){ icon = ''; } return icon; } }, createdCell: function(cell, cellData, rowData, rowIndex, colIndex){ let tableApi = this.api(); if($(cell).find('.fa-ban').length){ $(cell).removeClass(config.tableCellActionClass + ' ' + config.moduleHeadlineIconClass); $(cell).find('i').tooltip(); }else{ let confirmationSettings = { title: 'delete structure', template: Util.getConfirmationTemplate(null, { size: 'small', noTitle: true }), onConfirm : function(e, target){ // get current row data (important!) // -> "rowData" param is not current state, values are "on createCell()" state rowData = tableApi.row( $(cell).parents('tr')).data(); // let deleteRowElement = $(cell).parents('tr'); // tableApi.rows(deleteRowElement).remove().draw(); moduleElement.showLoadingAnimation(); Util.request('DELETE', 'structure', rowData.id, {}, { moduleElement: moduleElement, tableApi: tableApi }, context => context.moduleElement.hideLoadingAnimation() ).then( payload => callbackDeleteStructures(payload.context, payload.data), Util.handleAjaxErrorResponse ); } }; // init confirmation dialog $(cell).confirmation(confirmationSettings); } } },{ targets: 10, name: 'rowGroupData', className: 'never', // never show this column. see: https://datatables.net/extensions/responsive/classes data: 'rowGroupData', visible: false, render: { sort: function(data){ return data.name; } } } ], initComplete: function(settings){ // table data is load in updateModule() method // -> no need to trigger additional ajax call here for data // -> in case table update failed -> each if this initComplete() function finished before table updated // e.g. return now promise in getModule() function Counter.initTableCounter(this, ['updated:name'], 'd'); } }; $.extend(true, structureDataTableOptions, getDataTableDefaults()); let tableApiStructure = structureTable.DataTable(structureDataTableOptions); new $.fn.dataTable.Responsive(tableApiStructure); tableApiStructure.on('responsive-resize', function(e, tableApi, columns){ // rowGroup length changes as well -> trigger draw() updates rowGroup length (see drawCallback()) tableApi.draw(); }); if(showStationTable){ // "Stations" table --------------------------------------------------------------------------------------- moduleElement.append( $('
', { class: config.moduleHeadClass }).append( $('
', { class: config.moduleHandlerClass }), $('
', { text: 'Stations' }) ) ); let stationTable = $('
', { id: getTableId('station', mapId, systemData.id), class: ['compact', 'stripe', 'order-column', 'row-border', 'pf-table-fixed', config.systemStationsTableClass].join(' ') }); moduleElement.append(stationTable); let stationDataTableOptions = { pfMeta: { type: 'stations' }, order: [[1, 'asc' ], [8, 'asc' ]], rowId: rowData => getRowId('stations', rowData.id), language: { emptyTable: 'No stations found', info: '_START_ to _END_ of _MAX_', infoEmpty: '' }, columnDefs: [ { targets: 0, name: 'stationImage', title: '', width: 24, orderable: false, className: [config.tableCellImageClass, 'text-center', 'all'].join(' '), data: 'type.id', defaultContent: '', render: { _: function(data, type, row, meta){ let value = data; if(type === 'display' && value){ value = ''; } return value; } } },{ targets: 1, name: 'count', title: '', width: 5, className: ['text-center', 'all'].join(' '), data: 'name', render: { _: function(cellData, type, rowData, meta){ let value = ''; if(cellData){ // "grouped" regex not supported by FF // let matches = /^(?[a-z0-9\s\-]+) (?[MDCLXVI]+) .*$/i.exec(cellData); // let count = Util.getObjVal(matches, 'groups.count'); let matches = /^([a-z0-9\s\-]+) ([MDCLXVI]+) .*$/i.exec(cellData); let count = Util.getObjVal(matches, '2'); if(type === 'display'){ value = count || 0; }else{ value = romanToInt(count) || ''; } } return value; } } },{ targets: 2, name: 'name', title: 'station', className: [config.tableCellEllipsisClass, 'all'].join(' '), data: 'name', render: { _: function(cellData, type, rowData, meta){ let value = cellData; if(cellData){ // "grouped" regex not supported by FF // let matches = /^(?[a-z0-9\s\-]+) (?[MDCLXVI]+) (?