/** * System route module */ define([ 'jquery', 'app/init', 'app/util', 'bootbox', 'app/map/util' ], ($, Init, Util, bootbox, MapUtil) => { 'use strict'; let config = { // module info modulePosition: 1, moduleName: 'systemRoute', moduleHeadClass: 'pf-module-head', // class for module header moduleHandlerClass: 'pf-module-handler-drag', // class for "drag" handler // system route module moduleTypeClass: 'pf-system-route-module', // class for this module // headline toolbar moduleHeadlineIconClass: 'pf-module-icon-button', // class for toolbar icons in the head moduleHeadlineIconSearchClass: 'pf-module-icon-button-search', // class for "search" icon moduleHeadlineIconSettingsClass: 'pf-module-icon-button-settings', // class for "settings" icon moduleHeadlineIconRefreshClass: 'pf-module-icon-button-refresh', // class for "refresh" icon systemSecurityClassPrefix: 'pf-system-security-', // prefix class for system security level (color) // dialog routeSettingsDialogId: 'pf-route-settings-dialog', // id for route "settings" dialog routeDialogId: 'pf-route-dialog', // id for route "search" dialog systemDialogSelectClass: 'pf-system-dialog-select', // class for system select Element systemInfoRoutesTableClass: 'pf-system-route-table', // class for route tables routeDialogMapSelectId: 'pf-route-dialog-map-select', // id for "map" select routeDialogSizeSelectId: 'pf-route-dialog-size-select', // id for "wh size" select dataTableActionCellClass: 'pf-table-action-cell', // class for "action" cells dataTableRouteCellClass: 'pf-table-route-cell', // class for "route" cells dataTableJumpCellClass: 'pf-table-jump-cell', // class for "route jump" cells rallyClass: 'pf-rally', // class for "rally point" style routeCacheTTL: 5 // route cache timer (client) in seconds }; // cache for system routes let cache = { systemRoutes: {}, // jump information between solar systems mapConnections: {} // connection data read from UI }; /** * set cache data * @param cacheType * @param cacheKey * @param data */ let setCacheData = (cacheType, cacheKey, data) => { cache[cacheType][cacheKey] = { data: data, updated: Util.getServerTime().getTime() / 1000 }; }; /** * get cache data * @param cacheType * @param cacheKey * @returns {*} */ let getCacheData = (cacheType, cacheKey) => { let cachedData = null; let currentTimestamp = Util.getServerTime().getTime(); if( cache[cacheType].hasOwnProperty(cacheKey) && Math.round( ( currentTimestamp - (new Date( cache[cacheType][cacheKey].updated * 1000).getTime())) / 1000 ) <= config.routeCacheTTL ){ cachedData = cache[cacheType][cacheKey].data; } return cachedData; }; /** * @param mapIds * @param sourceName * @param targetName * @returns {string} */ let getRouteDataCacheKey = (mapIds, sourceName, targetName) => { return [mapIds.join('_'), sourceName.toLowerCase(), targetName.toLowerCase()].join('###'); }; /** * get a unique cache key name for "source"/"target"-name * @param sourceName * @param targetName * @returns {*} */ let getConnectionDataCacheKey = (sourceName, targetName) => { let key = false; if(sourceName && targetName){ // names can be "undefined" in case system is currently on drag/drop key = [sourceName.toLowerCase(), targetName.toLowerCase()].sort().join('###'); } return key; }; /** * callback function, adds new row to a dataTable with jump information for a route * @param context * @param routesData */ let callbackAddRouteRows = (context, routesData) => { if(routesData.length > 0){ for(let routeData of routesData){ // format routeData let rowData = formatRouteData(routeData); if(rowData.route){ // update route cache let cacheKey = getRouteDataCacheKey(rowData.mapIds, routeData.systemFromData.name, routeData.systemToData.name); setCacheData('systemRoutes', cacheKey, rowData); addRow(context, rowData); } } // redraw dataTable context.dataTable.draw(); } }; /** * add a new dataTable row to the routes table * @param context * @param rowData * @returns {*} */ let addRow = (context, rowData) => { let dataTable = context.dataTable; let rowElement = null; let row = null; let animationStatus = 'changed'; // search for an existing row (e.g. on mass "table refresh" [all routes]) // get rowIndex where column 1 (equals to "systemToData.name") matches rowData.systemToData.name let indexes = dataTable.rows().eq(0).filter((rowIdx) => { return (dataTable.cell(rowIdx, 1).data().name === rowData.systemToData.name); }); if(indexes.length > 0){ // update row with FIRST index // -> systemFrom should be unique! row = dataTable.row( parseInt(indexes[0]) ); // update row data row.data(rowData); }else{ // no existing route found -> add new row row = dataTable.row.add( rowData ); animationStatus = 'added'; } if(row.length > 0){ rowElement = row.nodes().to$(); rowElement.data('animationStatus', animationStatus); rowElement.initTooltips({ container: 'body' }); } return rowElement; }; /** * requests route data from eveCentral API and execute callback * @param requestData * @param context * @param callback */ let getRouteData = (requestData, context, callback) => { context.moduleElement.showLoadingAnimation(); $.ajax({ url: Init.path.searchRoute, type: 'POST', dataType: 'json', data: requestData, context: context }).done(function(routesData){ this.moduleElement.hideLoadingAnimation(); // execute callback callback(this, routesData.routesData); }); }; /** * update complete routes table (refresh all) * @param moduleElement * @param dataTable */ let updateRoutesTable = (moduleElement, dataTable) => { let context = { moduleElement: moduleElement, dataTable: dataTable }; let routeData = []; dataTable.rows().every(function(){ routeData.push(getRouteRequestDataFromRowData(this.data())); }); getRouteData({routeData: routeData}, context, callbackAddRouteRows); }; /** * format rowData for route search/update request * @param {Object} rowData * @returns {Object} */ let getRouteRequestDataFromRowData = rowData => { return { mapIds: (rowData.hasOwnProperty('mapIds')) ? rowData.mapIds : [], systemFromData: (rowData.hasOwnProperty('systemFromData')) ? rowData.systemFromData : {}, systemToData: (rowData.hasOwnProperty('systemToData')) ? rowData.systemToData : {}, skipSearch: (rowData.hasOwnProperty('skipSearch')) ? rowData.skipSearch | 0 : 0, stargates: (rowData.hasOwnProperty('stargates')) ? rowData.stargates | 0 : 1, jumpbridges: (rowData.hasOwnProperty('jumpbridges')) ? rowData.jumpbridges | 0 : 1, wormholes: (rowData.hasOwnProperty('wormholes')) ? rowData.wormholes | 0 : 1, wormholesReduced: (rowData.hasOwnProperty('wormholesReduced')) ? rowData.wormholesReduced | 0 : 1, wormholesCritical: (rowData.hasOwnProperty('wormholesCritical')) ? rowData.wormholesCritical | 0 : 1, wormholesEOL: (rowData.hasOwnProperty('wormholesEOL')) ? rowData.wormholesEOL | 0 : 1, wormholesSizeMin: (rowData.hasOwnProperty('wormholesSizeMin')) ? rowData.wormholesSizeMin : '', excludeTypes: (rowData.hasOwnProperty('excludeTypes')) ? rowData.excludeTypes : [], endpointsBubble: (rowData.hasOwnProperty('endpointsBubble')) ? rowData.endpointsBubble | 0 : 1, connections: (rowData.hasOwnProperty('connections')) ? rowData.connections.value | 0 : 0, flag: (rowData.hasOwnProperty('flag')) ? rowData.flag.value : 'shortest' }; }; /** * show route dialog. User can search for systems and jump-info for each system is added to a data table * @param dialogData */ let showFindRouteDialog = dialogData => { let mapSelectOptions = []; let currentMapData = Util.getCurrentMapData(); if(currentMapData !== false){ for(let i = 0; i < currentMapData.length; i++){ mapSelectOptions.push({ id: currentMapData[i].config.id, name: currentMapData[i].config.name, selected: (dialogData.mapId === currentMapData[i].config.id) }); } } let sizeOptions = MapUtil.allConnectionJumpMassTypes().map(type => { return { id: type, name: type, selected: false }; }); let data = { id: config.routeDialogId, select2Class: Util.config.select2Class, selectClass: config.systemDialogSelectClass, routeDialogMapSelectId: config.routeDialogMapSelectId, routeDialogSizeSelectId: config.routeDialogSizeSelectId, systemFromData: dialogData.systemFromData, systemToData: dialogData.systemToData, mapSelectOptions: mapSelectOptions, sizeOptions: sizeOptions }; requirejs(['text!templates/dialog/route.html', 'mustache'], (template, Mustache) => { let content = Mustache.render(template, data); let findRouteDialog = bootbox.dialog({ title: 'Route finder', message: content, show: false, buttons: { close: { label: 'cancel', className: 'btn-default' }, success: { label: ' search route', className: 'btn-primary', callback: function(){ // add new route to route table // get form Values let form = $('#' + config.routeDialogId).find('form'); let routeDialogData = $(form).getFormValues(); // validate form form.validator('validate'); // check whether the form is valid let formValid = form.isValidForm(); if(formValid === false){ // don't close dialog return false; } // get all system data from select2 // -> we could also get value from "routeDialogData" var, but we need systemName also let systemSelectData = form.find('.' + config.systemDialogSelectClass).select2('data'); if( systemSelectData && systemSelectData.length === 1 ){ let context = { moduleElement: dialogData.moduleElement, dataTable: dialogData.dataTable }; let requestData = { routeData: [{ mapIds: routeDialogData.mapIds, systemFromData: dialogData.systemFromData, systemToData: { systemId: parseInt(systemSelectData[0].id), name: systemSelectData[0].text }, stargates: routeDialogData.hasOwnProperty('stargates') ? parseInt(routeDialogData.stargates) : 0, jumpbridges: routeDialogData.hasOwnProperty('jumpbridges') ? parseInt(routeDialogData.jumpbridges) : 0, wormholes: routeDialogData.hasOwnProperty('wormholes') ? parseInt(routeDialogData.wormholes) : 0, wormholesReduced: routeDialogData.hasOwnProperty('wormholesReduced') ? parseInt(routeDialogData.wormholesReduced) : 0, wormholesCritical: routeDialogData.hasOwnProperty('wormholesCritical') ? parseInt(routeDialogData.wormholesCritical) : 0, wormholesEOL: routeDialogData.hasOwnProperty('wormholesEOL') ? parseInt(routeDialogData.wormholesEOL) : 0, wormholesSizeMin: routeDialogData.wormholesSizeMin || '', excludeTypes: getLowerSizeConnectionTypes(routeDialogData.wormholesSizeMin), endpointsBubble: routeDialogData.hasOwnProperty('endpointsBubble') ? parseInt(routeDialogData.endpointsBubble) : 0 }] }; getRouteData(requestData, context, callbackAddRouteRows); } } } } }); findRouteDialog.on('show.bs.modal', function(e){ findRouteDialog.initTooltips(); // init some dialog/form observer setDialogObserver( $(this) ); // init map select ------------------------------------------------------------------------------------ let mapSelect = findRouteDialog.find('#' + config.routeDialogMapSelectId); mapSelect.initMapSelect(); // init connection jump size select ------------------------------------------------------------------- findRouteDialog.find('#' + config.routeDialogSizeSelectId).initConnectionSizeSelect(); }); findRouteDialog.on('shown.bs.modal', function(e){ // init system select live search -------------------------------------------------------------------- // -> add some delay until modal transition has finished let systemTargetSelect = $(this).find('.' + config.systemDialogSelectClass); systemTargetSelect.delay(240).initSystemSelect({key: 'id'}); }); // show dialog findRouteDialog.modal('show'); }); }; /** * draw route table * @param mapId * @param moduleElement * @param systemFromData * @param routesTable * @param systemsToData */ let drawRouteTable = (mapId, moduleElement, systemFromData, routesTable, systemsToData) => { let requestRouteData = []; // Skip some routes from search // -> this should help to throttle requests (heavy CPU load for route calculation) let defaultRoutesCount = Init.routeSearch.defaultCount; let rowElements = []; for(let systemToData of systemsToData){ if(systemFromData.name !== systemToData.name){ // check for cached rowData let cacheKey = getRouteDataCacheKey([mapId], systemFromData.name, systemToData.name); let rowData = getCacheData('systemRoutes', cacheKey); if(rowData){ // route data is cached (client side) let context = { dataTable: routesTable }; rowElements.push( addRow(context, rowData) ); }else{ // get route data -> ajax let searchData = { mapIds: [mapId], systemFromData: systemFromData, systemToData: systemToData, skipSearch: requestRouteData.length >= defaultRoutesCount }; requestRouteData.push(getRouteRequestDataFromRowData(searchData)); } } } // rows added from cache -> redraw() table if(rowElements.length){ routesTable.draw(); } // check if routes data is not cached and is requested if(requestRouteData.length > 0){ let contextData = { moduleElement: moduleElement, dataTable: routesTable }; let requestData = { routeData: requestRouteData }; getRouteData(requestData, contextData, callbackAddRouteRows); } }; /** * show route settings dialog * @param dialogData * @param moduleElement * @param systemFromData * @param routesTable */ let showSettingsDialog = (dialogData, moduleElement, systemFromData, routesTable) => { let promiseStore = MapUtil.getLocaleData('map', dialogData.mapId); promiseStore.then(dataStore => { // selected systems (if already stored) let systemSelectOptions = []; if( dataStore && dataStore.routes ){ systemSelectOptions = dataStore.routes; } // max count of "default" target systems let maxSelectionLength = Init.routeSearch.maxDefaultCount; let data = { id: config.routeSettingsDialogId, selectClass: config.systemDialogSelectClass, systemSelectOptions: systemSelectOptions, maxSelectionLength: maxSelectionLength }; requirejs(['text!templates/dialog/route_settings.html', 'mustache'], (template, Mustache) => { let content = Mustache.render(template, data); let settingsDialog = bootbox.dialog({ title: 'Route settings', message: content, show: false, buttons: { close: { label: 'cancel', className: 'btn-default' }, success: { label: ' save', className: 'btn-success', callback: function(){ let form = this.find('form'); // get all system data from select2 let systemSelectData = form.find('.' + config.systemDialogSelectClass).select2('data'); let systemsTo = []; if( systemSelectData.length > 0 ){ systemsTo = formSystemSelectData(systemSelectData); MapUtil.storeLocalData('map', dialogData.mapId, 'routes', systemsTo); }else{ MapUtil.deleteLocalData('map', dialogData.mapId, 'routes'); } Util.showNotify({title: 'Route settings stored', type: 'success'}); // (re) draw table drawRouteTable(dialogData.mapId, moduleElement, systemFromData, routesTable, systemsTo); } } } }); settingsDialog.on('shown.bs.modal', function(e){ // init default system select --------------------------------------------------------------------- // -> add some delay until modal transition has finished let systemTargetSelect = $(this).find('.' + config.systemDialogSelectClass); systemTargetSelect.delay(240).initSystemSelect({key: 'id', maxSelectionLength: maxSelectionLength}); }); // show dialog settingsDialog.modal('show'); }); }); }; /** * format select2 system data * @param {Array} data * @returns {Array} */ let formSystemSelectData = (data) => { let formattedData = []; for(let i = 0; i < data.length; i++){ let tmpData = data[i]; formattedData.push({ systemId: parseInt(tmpData.id), name: tmpData.text }); } return formattedData; }; /** * set event observer for route finder dialog * @param routeDialog */ let setDialogObserver = routeDialog => { let wormholeCheckbox = routeDialog.find('input[type="checkbox"][name="wormholes"]'); let wormholeReducedCheckbox = routeDialog.find('input[type="checkbox"][name="wormholesReduced"]'); let wormholeCriticalCheckbox = routeDialog.find('input[type="checkbox"][name="wormholesCritical"]'); let wormholeEolCheckbox = routeDialog.find('input[type="checkbox"][name="wormholesEOL"]'); let wormholeSizeSelect = routeDialog.find('#' + config.routeDialogSizeSelectId); // store current "checked" state for each box --------------------------------------------- let storeCheckboxStatus = function(){ wormholeReducedCheckbox.data('selectState', wormholeReducedCheckbox.prop('checked')); wormholeCriticalCheckbox.data('selectState', wormholeCriticalCheckbox.prop('checked')); wormholeEolCheckbox.data('selectState', wormholeEolCheckbox.prop('checked')); }; // on wormhole checkbox change ------------------------------------------------------------ let onWormholeCheckboxChange = function(){ if( $(this).is(':checked') ){ wormholeSizeSelect.prop('disabled', false); wormholeReducedCheckbox.prop('disabled', false); wormholeCriticalCheckbox.prop('disabled', false); wormholeEolCheckbox.prop('disabled', false); wormholeReducedCheckbox.prop('checked', wormholeReducedCheckbox.data('selectState')); wormholeCriticalCheckbox.prop('checked', wormholeCriticalCheckbox.data('selectState')); wormholeEolCheckbox.prop('checked', wormholeEolCheckbox.data('selectState')); }else{ wormholeSizeSelect.prop('disabled', true); storeCheckboxStatus(); wormholeReducedCheckbox.prop('checked', false); wormholeReducedCheckbox.prop('disabled', true); wormholeCriticalCheckbox.prop('checked', false); wormholeCriticalCheckbox.prop('disabled', true); wormholeEolCheckbox.prop('checked', false); wormholeEolCheckbox.prop('disabled', true); } }.bind(wormholeCheckbox); wormholeCheckbox.on('change', onWormholeCheckboxChange); // initial checkbox check storeCheckboxStatus(); onWormholeCheckboxChange(); }; /** * get a connectionsData object that holds all connections for given mapIds (used as cache for route search) * @param mapIds * @returns {{}} */ let getConnectionsDataFromMaps = (mapIds) => { let connectionsData = {}; for(let mapId of mapIds){ let map = MapUtil.getMapInstance(mapId); if(map){ let cacheKey = 'map_' + mapId; let mapConnectionsData = getCacheData('mapConnections', cacheKey); if(!mapConnectionsData){ mapConnectionsData = {}; let connections = map.getAllConnections(); if(connections.length){ let connectionsData = MapUtil.getDataByConnections(connections); for(let connectionData of connectionsData){ let connectionDataCacheKey = getConnectionDataCacheKey(connectionData.sourceName, connectionData.targetName); // skip double connections between same systems if( !mapConnectionsData.hasOwnProperty(connectionDataCacheKey) ){ mapConnectionsData[connectionDataCacheKey] = { map: { id: mapId }, connection: { id: connectionData.id, type: connectionData.type, scope: connectionData.scope }, source: { id: connectionData.source, name: connectionData.sourceName, alias: connectionData.sourceAlias }, target: { id: connectionData.target, name: connectionData.targetName, alias: connectionData.targetAlias } }; } } } // update cache setCacheData('mapConnections', cacheKey, mapConnectionsData); } if(connectionsData !== null){ connectionsData = Object.assign({}, mapConnectionsData, connectionsData); } } } return connectionsData; }; /** * search for a specific connection by "source"/"target"-name inside connectionsData cache * @param connectionsData * @param sourceName * @param targetName * @returns {{}} */ let findConnectionsData = (connectionsData, sourceName, targetName) => { let connectionDataCacheKey = getConnectionDataCacheKey(sourceName, targetName); return connectionsData.hasOwnProperty(connectionDataCacheKey) ? connectionsData[connectionDataCacheKey] : {}; }; /** * get stargate connection data (default connection type in case connection was not found on a map) * @param sourceRouteNodeData * @param targetRouteNodeData * @returns {{connection: {id: number, type: string[], scope: string}, source: {id: number, name, alias}, target: {id: number, name, alias}}} */ let getStargateConnectionData = (sourceRouteNodeData, targetRouteNodeData) => { return { connection: { id: 0, type: ['stargate'], scope: 'stargate' }, source: { id: 0, name: sourceRouteNodeData.system, alias: sourceRouteNodeData.system }, target: { id: 0, name: targetRouteNodeData.system, alias: targetRouteNodeData.system } }; }; /** * get fake connection Element * @param connectionData * @returns {string} */ let getFakeConnectionElement = (connectionData) => { let mapId = Util.getObjVal(connectionData, 'map.id') | 0; let connectionId = Util.getObjVal(connectionData, 'connection.id') | 0; let scope = Util.getObjVal(connectionData, 'connection.scope'); let classes = MapUtil.getConnectionFakeClassesByTypes(connectionData.connection.type); let disabled = !mapId || !connectionId; let connectionElement = '
'; return connectionElement; }; /** * format route data from API request into dataTable row format * @param routeData * @returns {{}} */ let formatRouteData = (routeData) => { /** * get status icon for route * @param status * @returns {string} */ let getStatusIcon= (status) => { let color = 'txt-color-danger'; let title = 'route not found'; switch(status){ case 1: color = 'txt-color-success'; title = 'route exists'; break; case 2: color = 'txt-color-warning'; title = 'not search performed'; break; } return ''; }; // route status: // 0: not found // 1: round (OK) // 2: not searched let routeStatus = routeData.skipSearch ? 2 : 0; // button class for flag (e.g. "secure" routes) let flagButtonClass = routeData.flag === 'secure' ? 'txt-color-success' : ''; let connectionButton = ''; let flagButton = ''; let reloadButton = ''; let searchButton = ''; let deleteButton = ''; // default row data (e.g. no route found) let tableRowData = { systemFromData: routeData.systemFromData, systemToData: routeData.systemToData, jumps: { value: 9999, // for sorting formatted: '' }, avgTrueSec: { value: '', formatted: '' }, route: { value: routeStatus === 2 ? 'search now' : 'not found', data: routeData.route }, stargates: routeData.stargates, jumpbridges: routeData.jumpbridges, wormholes: routeData.wormholes, wormholesReduced: routeData.wormholesReduced, wormholesCritical: routeData.wormholesCritical, wormholesEOL: routeData.wormholesEOL, wormholesSizeMin: routeData.wormholesSizeMin, excludeTypes: routeData.excludeTypes, endpointsBubble: routeData.endpointsBubble, connections: { value: 0, button: connectionButton }, flag: { value: routeData.flag, button: flagButton }, reload: { button: routeData.skipSearch ? searchButton : reloadButton }, clear: { button: deleteButton }, maps: routeData.maps, mapIds: routeData.mapIds //map data (mapIds is "redundant") }; if( routeData.routePossible === true && routeData.route.length > 0 ){ // route data available routeStatus = 1; // add route Data let routeJumpElements = []; let avgSecTemp = 0; let connectionsData = getConnectionsDataFromMaps(routeData.mapIds); let prevRouteNodeData = null; // loop all systems on this route for(let i = 0; i < routeData.route.length; i++){ let routeNodeData = routeData.route[i]; let systemName = routeNodeData.system; // fake connection elements between systems ----------------------------------------------------------- if(prevRouteNodeData){ let connectionData = findConnectionsData(connectionsData, prevRouteNodeData.system, systemName); if(!connectionData.hasOwnProperty('connection')){ connectionData = getStargateConnectionData(prevRouteNodeData, routeNodeData); } let connectionElement = getFakeConnectionElement(connectionData); routeJumpElements.push( connectionElement ); } // system elements ------------------------------------------------------------------------------------ let systemSec = Number(routeNodeData.security).toFixed(1).toString(); let tempSystemSec = systemSec; if(tempSystemSec <= 0){ tempSystemSec = '0-0'; } let systemSecClass = config.systemSecurityClassPrefix + tempSystemSec.replace('.', '-'); // check for wormhole let icon = 'fas fa-square'; if( /^J\d+$/.test(systemName) ){ icon = 'fas fa-dot-circle'; } let system = ''; routeJumpElements.push( system ); // "source" system is not relevant for average security if(i > 0){ avgSecTemp += Number(routeNodeData.security); } prevRouteNodeData = routeNodeData; } let avgSec = ( avgSecTemp / (routeData.route.length - 1)).toFixed(2); let avgSecForClass = Number(avgSec).toFixed(1); if(avgSecForClass <= 0){ avgSecForClass = '0.0'; } let avgSecClass = config.systemSecurityClassPrefix + avgSecForClass.toString().replace('.', '-'); tableRowData.jumps = { value: routeData.routeJumps, formatted: routeData.routeJumps }; tableRowData.avgTrueSec = { value: avgSec, formatted: '' + avgSec + '' }; tableRowData.route.value = routeJumpElements.join(' '); } // route status data ------------------------------------------------------------------------------------------ tableRowData.status = { value: routeStatus, formatted: getStatusIcon(routeStatus) }; return tableRowData; }; /** * get module element * @param parentElement * @param mapId * @param systemData * @returns {jQuery} */ let getModule = (parentElement, mapId, systemData) => { let moduleElement = $('