/** * Main map functionality */ define([ 'jquery', 'app/init', 'app/util', 'app/key', 'app/lib/dragSelect', 'app/lib/eventHandler', 'bootbox', 'app/map/util', 'app/map/contextmenu', 'app/map/overlay/overlay', 'app/map/overlay/util', 'app/map/system', 'app/map/layout', 'app/map/magnetizing', 'app/map/scrollbar', 'app/map/local' ], ($, Init, Util, Key, DragSelect, EventHandler, bootbox, MapUtil, MapContextMenu, MapOverlay, MapOverlayUtil, System, Layout, Magnetizer, Scrollbar) => { 'use strict'; let config = { zIndexCounter: 110, maxActiveConnections: 8, mapIdPrefix: 'pf-map-', // id prefix for all maps systemClass: 'pf-system', // class for all systems systemSelectedClass: 'pf-system-selected', // class for selected systems on a map systemHeadClass: 'pf-system-head', // class for system head systemHeadNameClass: 'pf-system-head-name', // class for system name systemHeadCounterClass: 'pf-system-head-counter', // class for system user counter systemHeadExpandClass: 'pf-system-head-expand', // class for system head expand arrow systemHeadInfoClass: 'pf-system-head-info', // class for system info systemBodyClass: 'pf-system-body', // class for system body systemBodyItemHeight: 16, // px of a system body entry systemBodyItemClass: 'pf-system-body-item', // class for a system body entry systemBodyItemStatusClass: 'pf-user-status', // class for player status in system body systemBodyItemNameClass: 'pf-system-body-item-name', // class for player name in system body systemBodyRightClass: 'pf-system-body-right', // class for player ship name in system body // endpoint classes endpointSourceClass: 'pf-map-endpoint-source', endpointTargetClass: 'pf-map-endpoint-target', // system security classes systemSec: 'pf-system-sec' }; // active connections per map (cache object) let connectionCache = {}; // mapIds that receive updates while they are "locked" (active timer) // -> those maps queue their updates until "pf:unlocked" event let mapUpdateQueue = []; // map menu options let mapOptions = { mapMagnetizer: { buttonId: Util.config.menuButtonMagnetizerId, description: 'Magnetizer', onEnable: Magnetizer.initMagnetizer, onDisable: Magnetizer.destroyMagnetizer }, mapSnapToGrid : { buttonId: Util.config.menuButtonGridId, description: 'Grid snapping', class: 'mapGridClass' }, mapSignatureOverlays : { buttonId: Util.config.menuButtonEndpointId, description: 'Endpoint overlay', onEnable: MapOverlay.showInfoSignatureOverlays, onDisable: MapOverlay.hideInfoSignatureOverlays, }, mapCompact : { buttonId: Util.config.menuButtonCompactId, description: 'Compact system layout', class: 'mapCompactClass' } }; /** * checks mouse events on system head elements * -> prevents drag/drop system AND drag/drop connections on some child elements * @param e * @param system * @returns {boolean | *} */ let filterSystemHeadEvent = (e, system) => { let target = $(e.target); let effectClass = MapUtil.getEffectInfoForSystem('effect', 'class'); return ( target.hasClass(config.systemHeadNameClass) || target.hasClass(effectClass) || target.hasClass(config.systemHeadExpandClass) || target.hasClass(config.systemHeadInfoClass) ); }; // jsPlumb config let globalMapConfig = { source: { filter: filterSystemHeadEvent, //isSource:true, isTarget: true, // add target Endpoint to each system (e.g. for drag&drop) allowLoopback: false, // loopBack connections are not allowed cssClass: config.endpointSourceClass, uniqueEndpoint: false, // each connection has its own endpoint visible dragOptions:{ }, connectionsDetachable: true, // dragOptions are set -> allow detaching them maxConnections: 10, // due to isTarget is true, this is the max count of !out!-going connections //isSource:true }, target: { filter: filterSystemHeadEvent, isSource: true, //isTarget:true, //allowLoopBack: false, // loopBack connections are not allowed cssClass: config.endpointTargetClass, dropOptions: { //hoverClass: '', activeClass: 'dragActive' }, //uniqueEndpoint: false }, endpointTypes: Init.endpointTypes, connectionTypes: Init.connectionTypes }; /** * revalidate (repaint) all connections of el * -> in addition this re-calculates the Location of potential Endpoint Overlays * @param map * @param element (can also be an array) */ let revalidate = (map, element) => { map.revalidate(element); // get attached connections let elements = (typeof element === 'object' && element.length) ? element : [element]; for(let element of elements){ let connectionsInfo = map.anchorManager.getConnectionsFor(element.id); for(let connectionInfo of connectionsInfo){ // index 0 -> Connection, 1 -> Endpoint // -> we need BOTH endpoints of a connection -> index 0 for(let endpoint of connectionInfo[0].endpoints){ // check if there is a Label overlay let overlay = endpoint.getOverlay(MapOverlayUtil.config.endpointOverlayId); if(overlay instanceof jsPlumb.Overlays.Label){ let labels = overlay.getParameter('signatureLabels'); overlay.setLocation(MapUtil.getEndpointOverlaySignatureLocation(endpoint, labels)); } } } } }; /** * updates a system with current information * @param map * @param system * @param data * @param currentUserIsHere boolean - if the current user is in this system * @param options */ let updateSystemUserData = (map, system, data, currentUserIsHere = false, options = {}) => { let systemIdAttr = system.attr('id'); let compactView = Util.getObjVal(options, 'compactView'); // find countElement -> minimizedUI let systemCount = system.find('.' + config.systemHeadCounterClass); // find system body let systemBody = system.find('.' + config.systemBodyClass); // find expand arrow let systemHeadExpand = system.find('.' + config.systemHeadExpandClass); let oldCacheKey = system.data('userCacheKey'); let oldUserCount = system.data('userCount') || 0; let userWasHere = Boolean(system.data('currentUser')); let userCounter = 0; system.data('currentUser', currentUserIsHere); // auto select system if current user is in THIS system if( currentUserIsHere && !userWasHere && Boolean(Util.getObjVal(Init, 'character.autoLocationSelect')) && Boolean(Util.getCurrentCharacterData('selectLocation')) ){ Util.triggerMenuAction(map.getContainer(), 'SelectSystem', {systemId: system.data('id'), forceSelect: false}); } // add user information if( data && data.user ){ userCounter = data.user.length; // loop all active pilots and build cache-key let cacheArray = []; for(let tempUserData of data.user){ cacheArray.push(tempUserData.id + '_' + tempUserData.log.ship.typeId); } // make sure cacheArray values are sorted for key comparison let collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'}); cacheArray.sort(collator.compare); // we need to add "view mode" option to key // -> if view mode change detected -> key no longer valid let cacheKey = compactView ? 'compact' : 'default'; cacheKey += '_' + cacheArray.join('_').hashCode(); // check for if cacheKey has changed if(cacheKey !== oldCacheKey){ // set new CacheKey system.data('userCacheKey', cacheKey); system.data('userCount', userCounter); // remove all content systemBody.empty(); if(compactView){ // compact system layout-> pilot count shown in systemHead systemCount.text(userCounter); system.toggleSystemTooltip('destroy', {}); systemHeadExpand.hide(); system.toggleBody(false, map, {}); map.revalidate(systemIdAttr); }else{ systemCount.empty(); // show active pilots in body + pilots count tooltip // loop "again" and build DOM object with user information for(let j = 0; j < data.user.length; j++){ let userData = data.user[j]; let statusClass = Util.getStatusInfoForCharacter(userData, 'class'); let userName = userData.name; let item = $('
', { class: config.systemBodyItemClass }).append( $('', { text: userData.log.ship.typeName, class: config.systemBodyRightClass }) ).append( $('', { class: ['fas', 'fa-circle', config.systemBodyItemStatusClass, statusClass].join(' ') }) ).append( $('', { class: config.systemBodyItemNameClass, text: userName }) ); systemBody.append(item); } // user count changed -> change tooltip content let highlight = ''; if(userCounter >= oldUserCount){ highlight = 'good'; }else if(userCounter < oldUserCount){ highlight = 'bad'; } let tooltipOptions = { systemId: systemIdAttr, highlight: highlight, userCount: userCounter }; // show system head systemHeadExpand.css('display', 'inline-block'); // show system body system.toggleBody(true, map, { complete: function(system){ // show active user tooltip system.toggleSystemTooltip('show', tooltipOptions); map.revalidate(systemIdAttr); } }); } } }else{ // no user data found for this system system.data('userCacheKey', false); system.data('userCount', 0); systemBody.empty(); if( oldCacheKey && oldCacheKey.length > 0 ){ // reset all elements systemCount.empty(); system.toggleSystemTooltip('destroy', {}); systemHeadExpand.hide(); system.toggleBody(false, map, {}); map.revalidate(systemIdAttr); } } }; /** * show/hide system body element * @param type * @param map * @param callback */ $.fn.toggleBody = function(type, map, callback){ let system = $(this); let systemBody = system.find('.' + config.systemBodyClass); let systemDomId = system.attr('id'); if(type === true){ // show minimal body systemBody.velocity({ height: config.systemBodyItemHeight + 'px' },{ duration: 50, display: 'auto', progress: function(){ //re-validate element size and repaint map.revalidate( systemDomId ); }, complete: function(){ map.revalidate( systemDomId ); if(callback.complete){ callback.complete(system); } } }); }else if(type === false){ // hide body // remove all inline styles -> possible relict from previous hover-extend systemBody.velocity({ height: 0 + 'px', width: '100%', 'min-width': 'none' },{ duration: 50, display: 'none', begin: function(){ }, progress: function(){ // re-validate element size and repaint map.revalidate( systemDomId ); }, complete: function(){ map.revalidate( systemDomId ); } }); } }; /** * set or change the status of a system * @param status */ $.fn.setSystemStatus = function(status){ let system = $(this); let statusId = Util.getStatusInfoForSystem(status, 'id'); let statusClass = Util.getStatusInfoForSystem(status, 'class'); for(let property in Init.systemStatus){ if(Init.systemStatus.hasOwnProperty(property)){ system.removeClass( Init.systemStatus[property].class ); } } // add new class system.data('statusId', statusId); system.addClass( statusClass ); }; /** * returns a new system or updates an existing system * @param map * @param data * @returns {HTMLElement} */ $.fn.getSystem = function(map, data){ // get map container for mapId information let mapContainer = $(this); let systemId = MapUtil.getSystemId(mapContainer.data('id'), data.id); // check if system already exists let system = document.getElementById( systemId ); let newPosX = data.position.x + 'px'; let newPosY = data.position.y + 'px'; if(!system){ // set system name or alias let systemName = data.name; if( data.alias && data.alias !== '' ){ systemName = data.alias; } let systemHeadClasses = [config.systemHeadNameClass]; // Abyssal system if(data.type.id === 3){ systemHeadClasses.push(Util.config.fontTriglivianClass); } // get system info classes let effectBasicClass = MapUtil.getEffectInfoForSystem('effect', 'class'); let effectClass = MapUtil.getEffectInfoForSystem(data.effect, 'class'); let secClass = Util.getSecurityClassForSystem(data.security); system = $('
', { id: systemId, class: config.systemClass }).append( $('
', { class: config.systemHeadClass }).append( $('', { class: [config.systemSec, secClass].join(' '), text: data.security }), // System name is editable $('', { class: systemHeadClasses.join(' '), }).attr('data-value', systemName), // System users count $('', { class: [config.systemHeadCounterClass, Util.config.popoverTriggerClass].join(' ') }), // System locked status $('', { class: ['fas', 'fa-lock', 'fa-fw'].join(' ') }).attr('title', 'locked'), // System effect color $('', { class: ['fas', 'fa-square', 'fa-fw', effectBasicClass, effectClass, Util.config.popoverTriggerClass].join(' ') }), // expand option $('', { class: ['fas', 'fa-angle-down', config.systemHeadExpandClass].join(' ') }), // info element (new line) (optional) System.getHeadInfoElement(data) ), $('
', { class: config.systemBodyClass }) ); // set initial system position system.css({ 'left': newPosX, 'top': newPosY }); }else{ system = $(system); // set system position let currentPosX = system.css('left'); let currentPosY = system.css('top'); if( newPosX !== currentPosX || newPosY !== currentPosY ){ // change position with animation system.velocity( { left: newPosX, top: newPosY },{ easing: 'linear', duration: Init.animationSpeed.mapMoveSystem, begin: function(system){ // hide system tooltip $(system).toggleSystemTooltip('hide', {}); // destroy popovers $(system).destroyPopover(true); // move them to the "top" $(system).updateSystemZIndex(); }, progress: function(system){ revalidate(map, system); }, complete: function(system){ // show tooltip $(system).toggleSystemTooltip('show', {show: true}); revalidate(map, system); } } ); } // set system alias let alias = system.getSystemInfo(['alias']); if(alias !== data.alias){ // alias changed alias = data.alias ? data.alias : data.name; system.find('.' + config.systemHeadNameClass).editable('setValue', alias); } } // set system status system.setSystemStatus(data.status.name); system.data('id', parseInt(data.id)); system.data('systemId', parseInt(data.systemId)); system.data('name', data.name); system.data('typeId', parseInt(data.type.id)); system.data('effect', data.effect); system.data('security', data.security); system.data('trueSec', parseFloat(data.trueSec)); system.data('regionId', parseInt(data.region.id)); system.data('region', data.region.name); system.data('constellationId', parseInt(data.constellation.id)); system.data('constellation', data.constellation.name); system.data('planets', data.planets); system.data('shattered', data.shattered); system.data('drifter', data.drifter); system.data('statics', data.statics); system.data('updated', parseInt(data.updated.updated)); system.data('changed', false); system.attr('data-mapid', parseInt(mapContainer.data('id'))); if(data.sovereignty){ system.data('sovereignty', data.sovereignty); } // locked system if( Boolean(system.data('locked')) !== data.locked ){ system.toggleLockSystem(false, {hideNotification: true, hideCounter: true, map: map}); } // rally system system.setSystemRally(data.rallyUpdated, { poke: data.rallyPoke || false, hideNotification: true, hideCounter: true, }); return system; }; /** * system actions (e.g. for contextmenu) * @param action * @param system */ let systemActions = (action, system) => { let mapContainer = system.closest('.' + Util.config.mapClass); let map = MapUtil.getMapInstance(system.attr('data-mapid')); let systemData = {}; switch(action){ case 'add_system': // add a new system System.showNewSystemDialog(map, {sourceSystem: system}, saveSystemCallback); break; case 'lock_system': // lock system system.toggleLockSystem(true, {map: map}); // repaint connections, -> system changed its size! map.repaint(system); MapUtil.markAsChanged(system); break; case 'set_rally': // toggle rally point if(!system.data('rallyUpdated')){ $.fn.showRallyPointDialog(system); }else{ // remove rally point system.setSystemRally(0); MapUtil.markAsChanged(system); } break; case 'find_route': // show find route dialog systemData = system.getSystemData(); MapUtil.showFindRouteDialog(mapContainer, { systemId: systemData.systemId, name: systemData.name }); break; case 'select_connections': let connections = MapUtil.searchConnectionsBySystems(map, [system], '*'); MapUtil.showConnectionInfo(map, connections); break; case 'change_status_unknown': case 'change_status_friendly': case 'change_status_occupied': case 'change_status_hostile': case 'change_status_empty': case 'change_status_unscanned': // change system status MapOverlayUtil.getMapOverlay(system, 'timer').startMapUpdateCounter(); let statusString = action.split('_'); system.setSystemStatus(statusString[2]); MapUtil.markAsChanged(system); break; case 'delete_system': // delete this system AND delete selected systems as well let selectedSystems = mapContainer.getSelectedSystems(); $.merge(selectedSystems, system); $.uniqueSort(selectedSystems); $.fn.showDeleteSystemDialog(map, selectedSystems); break; case 'set_destination': case 'add_first_waypoint': case 'add_last_waypoint': systemData = system.getSystemData(); Util.setDestination(action, 'system', {id: systemData.systemId, name: systemData.name}); break; } }; /** * map actions (e.g. for contextmenu) * @param action * @param map * @param e */ let mapActions = (action, map, e) => { let mapElement = $(map.getContainer()); let mapId = parseInt(mapElement.data('id')); switch(action){ case 'add_system': // add new system dialog let position = Layout.getEventCoordinates(e); let dimensions = MapUtil.newSystemPositionByCoordinates(mapElement, { center: [position.x, position.y] }); if(dimensions.length){ position.x = dimensions[0].left; position.y = dimensions[0].top; } System.showNewSystemDialog(map, {position: position}, saveSystemCallback); break; case 'select_all': mapElement.selectAllSystems(); break; case 'filter_wh': case 'filter_stargate': case 'filter_jumpbridge': case 'filter_abyssal': // filter (show/hide) let filterScope = action.split('_')[1]; let filterScopeLabel = MapUtil.getScopeInfoForConnection( filterScope, 'label'); Util.getLocalStore('map').getItem(mapId).then(data => { let filterScopes = []; if(data && data.filterScopes){ filterScopes = data.filterScopes; } // add or remove this scope from filter let index = filterScopes.indexOf(filterScope); if(index >= 0){ filterScopes.splice(index, 1); }else{ filterScopes.push(filterScope); // "all filters active" == "no filter" if(filterScopes.length === Object.keys(Init.connectionScopes).length){ filterScopes = []; } } // store filterScopes in IndexDB Util.getLocalStore('map').setItem(`${mapId}.filterScopes`, filterScopes); MapUtil.filterMapByScopes(map, filterScopes); Util.showNotify({title: 'Scope filter changed', text: filterScopeLabel, type: 'success'}); }); break; case 'delete_systems': // delete all selected systems with its connections let selectedSystems = mapElement.getSelectedSystems(); $.fn.showDeleteSystemDialog(map, selectedSystems); break; case 'map_edit': // open map edit dialog tab Util.triggerMenuAction(document, 'ShowMapSettings', {tab: 'edit'}); break; case 'map_info': // open map info dialog tab Util.triggerMenuAction(document, 'ShowMapInfo', {tab: 'information'}); break; } }; /** * connection actions (e.g. for contextmenu) * @param action * @param connection */ let connectionActions = (action, connection) => { if(!connection._jsPlumb){ Util.showNotify({title: 'Connection not found', type: 'error'}); return; } let map = connection._jsPlumb.instance; let mapElement = $(map.getContainer()); let scope = connection.scope; let scopeName = MapUtil.getScopeInfoForConnection(scope, 'label'); switch(action){ case 'delete_connection': // delete a single connection bootbox.confirm('Is this connection really gone?', result => { if(result){ MapUtil.deleteConnections([connection]); } }); break; case 'preserve_mass': // set "preserve mass case 'wh_eol': // set "end of life" MapOverlayUtil.getMapOverlay(mapElement, 'timer').startMapUpdateCounter(); MapUtil.toggleConnectionType(connection, action); MapUtil.markAsChanged(connection); break; case 'status_fresh': case 'status_reduced': case 'status_critical': let newStatus = action.split('_')[1]; MapOverlayUtil.getMapOverlay(mapElement, 'timer').startMapUpdateCounter(); MapUtil.setConnectionMassStatusType(connection, 'wh_' + newStatus); MapUtil.markAsChanged(connection); break; case 'wh_jump_mass_s': case 'wh_jump_mass_m': case 'wh_jump_mass_l': case 'wh_jump_mass_xl': MapOverlayUtil.getMapOverlay(mapElement, 'timer').startMapUpdateCounter(); MapUtil.setConnectionJumpMassType(connection, action); MapUtil.markAsChanged(connection); break; case 'scope_wh': case 'scope_stargate': case 'scope_jumpbridge': let newScope = action.split('_')[1]; let newScopeName = MapUtil.getScopeInfoForConnection( newScope, 'label'); bootbox.confirm('Change scope from ' + scopeName + ' to ' + newScopeName + '?', result => { if(result){ setConnectionScope(connection, newScope); MapOverlayUtil.getMapOverlay(mapElement, 'timer').startMapUpdateCounter(); Util.showNotify({title: 'Connection scope changed', text: 'New scope: ' + newScopeName, type: 'success'}); MapUtil.markAsChanged(connection); } }); break; } }; /** * endpoint actions (e.g. for contextmenu) * @param action * @param endpoint */ let endpointActions = (action, endpoint) => { let map = endpoint._jsPlumb.instance; let mapElement = $(map.getContainer()); switch(action){ case 'bubble': MapOverlayUtil.getMapOverlay(mapElement, 'timer').startMapUpdateCounter(); endpoint.toggleType(action); for(let connection of endpoint.connections){ MapUtil.markAsChanged(connection); } break; } }; /** * click event handler for a Connection * @param connection * @param e */ let connectionClickHandler = (connection, e) => { if(e.which === 1){ let map = connection._jsPlumb.instance; if(e.ctrlKey === true){ // an "state_active" connection is required before adding more "selected" connections let activeConnections = MapUtil.getConnectionsByType(map, 'state_active'); if(activeConnections.length >= config.maxActiveConnections && !connection.hasType('state_active')){ Util.showNotify({title: 'Connection select limit', text: 'You can´t select more connections', type: 'warning'}); }else{ if(activeConnections.length > 0){ MapUtil.toggleConnectionActive(map, [connection]); }else{ MapUtil.showConnectionInfo(map, [connection]); } } }else{ MapUtil.showConnectionInfo(map, [connection]); } } }; /** * set/change connection scope * @param connection * @param scope */ let setConnectionScope = (connection, scope) => { let currentConnector = connection.getConnector(); let newConnector = MapUtil.getScopeInfoForConnection(scope, 'connectorDefinition'); if(currentConnector.type !== newConnector[0]){ // connector has changed connection.setConnector(newConnector); let map = connection._jsPlumb.instance; let oldScope = connection.scope; let oldTypes = MapUtil.filterDefaultTypes(connection.getType()); let newTypes = [MapUtil.getDefaultConnectionTypeByScope(scope)]; let removeTypes = oldTypes.intersect(MapUtil.filterDefaultTypes(Object.keys(map._connectionTypes)).diff(newTypes)); // remove all connection types that except some persistent types e.g. "state_process" MapUtil.removeConnectionTypes(connection, removeTypes); // set new new connection type for newScope MapUtil.addConnectionTypes(connection, newTypes); // change scope connection.scope = scope; console.info( 'connection "scope" changed for %O. Scope %o → %o, Types %o → %o', connection, oldScope, scope, oldTypes, newTypes ); } }; /** * connect two systems * @param map * @param connectionData * @returns {Promise} */ let drawConnection = (map, connectionData) => new Promise((resolve, reject) => { let mapContainer = $(map.getContainer()); let mapId = mapContainer.data('id'); let connectionId = connectionData.id || 0; let sourceSystem = $('#' + MapUtil.getSystemId(mapId, connectionData.source)); let targetSystem = $('#' + MapUtil.getSystemId(mapId, connectionData.target)); // check if both systems exists // (If not -> something went wrong e.g. DB-Foreign keys for "ON DELETE",...) if(!sourceSystem.length){ reject(new Error(`drawConnection(): source system (id: ${connectionData.source}) not found`)); }else if(!targetSystem.length){ reject(new Error(`drawConnection(): target system (id: ${connectionData.target}) not found`)); }else{ let connection = map.connect({ source: sourceSystem[0], target: targetSystem[0], scope: connectionData.scope || map.Defaults.Scope, //type: (connectionData.type || MapUtil.getDefaultConnectionTypeByScope(map.Defaults.Scope)).join(' ') /* experimental set "static" connection parameters in initial load parameters: { connectionId: connectionId, updated: connectionData.updated, created: connectionData.created, eolUpdated: connectionData.eolUpdated }*/ /* experimental (straight connections) anchors: [ [ "Perimeter", { shape: 'Rectangle' }], [ "Perimeter", { shape: 'Rectangle' }] ] */ }); // check if connection is valid (e.g. source/target exist if(connection instanceof jsPlumb.Connection){ connection.addType((connectionData.type || MapUtil.getDefaultConnectionTypeByScope(map.Defaults.Scope)).join(' ')); // set connection parameters // they should persist even through connection type change (e.g. wh -> stargate,..) // therefore they should be part of the connection not of the "Endpoint" or "connectionType" connection.setParameters({ connectionId: connectionId, updated: connectionData.updated, created: connectionData.created, eolUpdated: connectionData.eolUpdated }); if(connection.scope !== map.Defaults.Scope){ let newConnector = MapUtil.getScopeInfoForConnection(connection.scope, 'connectorDefinition'); connection.setConnector(newConnector); // we need to "reapply" the types after "Connector" was changed connection.reapplyTypes(); } // add endpoint types --------------------------------------------------------------------------------- if(connectionData.endpoints){ for(let endpoint of connection.endpoints){ let label = MapUtil.getEndpointLabel(connection, endpoint); if( label && connectionData.endpoints[label] && Array.isArray(connectionData.endpoints[label].types) ){ for(let type of connectionData.endpoints[label].types){ endpoint.addType(type); } } } } resolve({ action: 'drawConnection', data: { connection: connection } }); }else{ reject(new Error(`drawConnection(): connection must be instanceof jsPlumb.Connection`)); } } }); /** * compares the current data and new data of a connection and updates status * @param connection * @param newConnectionData * @returns {*} */ let updateConnection = (connection, newConnectionData) => { // check connection is currently dragged -> skip update if(connection.suspendedElement){ console.info( 'connection update skipped for %O. SuspendedElement: %o', connection, connection.suspendedElement ); return connection; } let currentConnectionData = MapUtil.getDataByConnection(connection); let map = connection._jsPlumb.instance; let mapContainer = $(map.getContainer()); let mapId = mapContainer.data('id'); // type "process" is not included in currentConnectionData ---------------------------------------------------- // -> if "process" type exists, remove it if(connection.hasType('state_process')){ MapUtil.removeConnectionTypes(connection, ['state_process']); } // check id, IDs should never change but must be set after initial save --------------------------------------- if(connection.getParameter('connectionId') !== newConnectionData.id){ connection.setParameter('connectionId', newConnectionData.id); } // update scope ----------------------------------------------------------------------------------------------- if(currentConnectionData.scope !== newConnectionData.scope){ setConnectionScope(connection, newConnectionData.scope); // connection type has changed as well -> get new connectionData for further process currentConnectionData = MapUtil.getDataByConnection(connection); } // update source/target (after drag&drop) --------------------------------------------------------------------- if(currentConnectionData.source !== newConnectionData.source){ map.setSource(connection, MapUtil.getSystemId(mapId, newConnectionData.source)); } if(currentConnectionData.target !== newConnectionData.target){ map.setTarget(connection, MapUtil.getSystemId(mapId, newConnectionData.target)); } // update connection types ==================================================================================== // update connection 'size' type ------------------------------------------------------------------------------ let allMassTypes = MapUtil.allConnectionJumpMassTypes(); let newMassTypes = allMassTypes.intersect(newConnectionData.type); let currentMassTypes = allMassTypes.intersect(currentConnectionData.type); if(!newMassTypes.equalValues(currentMassTypes)){ // connection 'size' type changed/removed // -> only ONE 'size' type is allowed -> take the first one MapUtil.setConnectionJumpMassType(connection, newMassTypes.length ? newMassTypes[0] : undefined); // connection type has changed as well -> get new connectionData for further process currentConnectionData = MapUtil.getDataByConnection(connection); } // update connection 'status' type ---------------------------------------------------------------------------- let allStatusTypes = MapUtil.allConnectionMassStatusTypes(); let newStatusTypes = allStatusTypes.intersect(newConnectionData.type); let currentStatusTypes = allStatusTypes.intersect(currentConnectionData.type); if(!newStatusTypes.equalValues(currentStatusTypes)){ // connection 'status' type changed/removed // -> only ONE 'status' type is allowed -> take the first one MapUtil.setConnectionMassStatusType(connection, newStatusTypes.length ? newStatusTypes[0] : undefined); // connection type has changed as well -> get new connectionData for further process currentConnectionData = MapUtil.getDataByConnection(connection); } // check for unhandled connection type changes ---------------------------------------------------------------- let allToggleTypes = ['wh_eol', 'preserve_mass']; let newTypes = allToggleTypes.intersect(newConnectionData.type.diff(currentConnectionData.type)); let oldTypes = allToggleTypes.intersect(currentConnectionData.type.diff(newConnectionData.type)); MapUtil.addConnectionTypes(connection, newTypes); MapUtil.removeConnectionTypes(connection, oldTypes); // update endpoints =========================================================================================== // important: In case source or target changed (drag&drop) (see above lines..) // -> NEW endpoints are created (default Endpoint properties from makeSource()/makeTarget() call are used // -> connectionData.endpoints might no longer be valid -> get fresh endpointData let endpointData = MapUtil.getEndpointsDataByConnection(connection); for(let endpoint of connection.endpoints){ let label = MapUtil.getEndpointLabel(connection, endpoint); let endpointTypes = Util.getObjVal(endpointData, [label, 'types'].join('.')) || []; let newEndpointTypes = Util.getObjVal(newConnectionData, ['endpoints', label, 'types'].join('.')) || []; let addEndpointTypes = newEndpointTypes.diff(endpointTypes); let removeEndpointTypes = endpointTypes.diff(newEndpointTypes); for(let type of addEndpointTypes){ endpoint.addType(type); } for(let type of removeEndpointTypes){ endpoint.removeType(type); } } // set update date (important for update check) =============================================================== // important: set parameters ONE-by-ONE! // -> (setParameters() will overwrite all previous params) connection.setParameter('created', newConnectionData.created); connection.setParameter('updated', newConnectionData.updated); connection.setParameter('eolUpdated', newConnectionData.eolUpdated); connection.setParameter('changed', false); return connection; }; /** * set map area observer * @param areaMap * @param mapConfig */ let setMapAreaObserver = (areaMap, mapConfig) => { /** * save current map dimension to local storage * @param entry */ let saveMapSize = entry => { let width = ''; let height = ''; if(entry.constructor.name === 'HTMLDivElement'){ width = entry.style.width; height = entry.style.height; }else if(entry.constructor.name === 'ResizeObserverEntry'){ width = entry.target.style.width; height = entry.target.style.height; } width = parseInt(width.substring(0, width.length - 2)) || 0; height = parseInt(height.substring(0, height.length - 2)) || 0; areaMap.trigger('pf:mapResize'); Util.getLocalStore('map').getItem(mapConfig.config.id).then((data) => { let storeData = true; if( data && data.style && data.style.width === width && data.style.height === height ){ // no style changes storeData = false; } if(storeData){ Util.getLocalStore('map').setItem(`${mapConfig.config.id}.style`, { width: width, height: height }); } }); }; // map resize observer ---------------------------------------------------------------------------------------- if(window.ResizeObserver){ // ResizeObserver() supported let resizeTimer; let wrapperResize = new ResizeObserver(entries => { // jshint ignore:line let checkMapSize = (entry) => { return setTimeout(saveMapSize, 100, entry); }; for(let entry of entries){ // use timeout to "throttle" save actions clearTimeout(resizeTimer); resizeTimer = checkMapSize(entry); } }); wrapperResize.observe(areaMap[0]); }else if(requestAnimationFrame){ // ResizeObserver() not supported let checkMapSize = (entry) => { saveMapSize(entry); return setTimeout(checkMapSize, 500, entry); }; checkMapSize(areaMap[0]); } }; /** * get a mapMapElement * @param areaMap * @param mapConfig * @returns {Promise} */ let newMapElement = (areaMap, mapConfig) => { areaMap = $(areaMap); /** * new map element promise * @param resolve * @param reject */ let newMapElementExecutor = (resolve, reject) => { // get map dimension from local storage Util.getLocalStore('map').getItem(mapConfig.config.id).then(data => { let height = 0; if(data && data.style){ height = data.style.height; } areaMap.css('height', height); setMapAreaObserver(areaMap, mapConfig); let mapId = mapConfig.config.id; // create new map container let mapContainer = $('
', { id: config.mapIdPrefix + mapId, class: Util.config.mapClass }).data('id', mapId); areaMap.append(mapContainer); // set main Container for current map -> the container exists now in DOM !! very important mapConfig.map.setContainer(mapContainer); // init custom scrollbars and add overlay initMapScrollbar(areaMap); // set map observer setMapObserver(mapConfig.map); // set shortcuts areaMap.setMapShortcuts(); // show static overlay actions let mapOverlay = MapOverlayUtil.getMapOverlay(mapContainer, 'info'); mapOverlay.updateOverlayIcon('systemRegion', 'show'); mapOverlay.updateOverlayIcon('connection', 'show'); mapOverlay.updateOverlayIcon('connectionEol', 'show'); resolve({ action: 'newMapElement', data: { mapConfig: mapConfig } }); }); }; return new Promise(newMapElementExecutor); }; /** * draw a new map or update an existing map with all its systems and connections * @param mapConfig * @returns {Promise} */ let updateMap = mapConfig => { /** * update map promise * @param resolve * @param reject */ let updateMapExecutor = (resolve, reject) => { let payload = { action: 'updateMap', data: { mapConfig: mapConfig } }; // jsPlumb needs to be initialized. This is not the case when switching between map tabs right after refresh let mapContainer = mapConfig.map ? mapConfig.map.getContainer() : null; if(!mapContainer){ return resolve(payload); } let mapId = mapConfig.config.id; // mapData == false -> map locked by update counter. Skip update let mapData = getMapDataForSync(mapContainer, [], true); if(!mapData){ // map is currently locked -> queue update for this map until unlock if(mapUpdateQueue.indexOf(mapId) === -1){ mapUpdateQueue.push(mapId); } return resolve(payload); } mapContainer = $(mapContainer); // add additional information for this map if(mapContainer.data('updated') !== mapConfig.config.updated.updated){ mapContainer.data('name', mapConfig.config.name); mapContainer.data('scopeId', mapConfig.config.scope.id); mapContainer.data('typeId', mapConfig.config.type.id); mapContainer.data('typeName', mapConfig.config.type.name); mapContainer.data('icon', mapConfig.config.icon); mapContainer.data('created', mapConfig.config.created.created); mapContainer.data('updated', mapConfig.config.updated.updated); } // map data available -> map not locked by update counter :) let currentSystemData = mapData.data.systems; let currentConnectionData = mapData.data.connections; // update systems ========================================================================================= for(let i = 0; i < mapConfig.data.systems.length; i++){ let systemData = mapConfig.data.systems[i]; // add system let addNewSystem = true; for(let k = 0; k < currentSystemData.length; k++){ if(currentSystemData[k].id === systemData.id){ if(currentSystemData[k].updated.updated < systemData.updated.updated){ // system changed -> update mapContainer.getSystem(mapConfig.map, systemData); } addNewSystem = false; break; } } if(addNewSystem === true){ drawSystem(mapConfig.map, systemData).catch(console.warn); } } // check for systems that are gone -> delete system for(let a = 0; a < currentSystemData.length; a++){ let deleteThisSystem = true; for(let b = 0; b < mapConfig.data.systems.length; b++){ let deleteSystemData = mapConfig.data.systems[b]; if(deleteSystemData.id === currentSystemData[a].id){ deleteThisSystem = false; break; } } if(deleteThisSystem === true){ let deleteSystem = $('#' + MapUtil.getSystemId(mapContainer.data('id'), currentSystemData[a].id)); // system not found -> delete system System.removeSystems(mapConfig.map, deleteSystem); } } // update connections ===================================================================================== // jsPlumb setSuspendDrawing() (batch() did not work because it async 'scopes' out updates). // -> Otherwise there are some "strange" visual bugs when switching maps (Endpoints are not displayed correctly) // -> needs to be "disabled" later in this method. mapConfig.map.setSuspendDrawing(true); for(let j = 0; j < mapConfig.data.connections.length; j++){ let connectionData = mapConfig.data.connections[j]; // add connection let addNewConnection= true; for(let c = 0; c < currentConnectionData.length; c++){ if(currentConnectionData[c].id === connectionData.id){ // connection already exists -> check for updates if(currentConnectionData[c].updated < connectionData.updated){ // connection changed -> update updateConnection(currentConnectionData[c].connection, connectionData); } addNewConnection = false; break; }else if( currentConnectionData[c].id === 0 && currentConnectionData[c].source === connectionData.source && currentConnectionData[c].target === connectionData.target ){ // if ids don´t match -> check for unsaved connection updateConnection(currentConnectionData[c].connection, connectionData); addNewConnection = false; break; } } if(addNewConnection === true){ drawConnection(mapConfig.map, connectionData).catch(console.warn); } } // check for connections that are gone -> delete connection for(let d = 0; d < currentConnectionData.length; d++){ // skip connections with id = 0 -> they might get updated before if(currentConnectionData[d].id === 0){ continue; } let deleteThisConnection = true; for(let e = 0; e < mapConfig.data.connections.length;e++){ let deleteConnectionData = mapConfig.data.connections[e]; if(deleteConnectionData.id === currentConnectionData[d].id){ deleteThisConnection = false; break; } } if(deleteThisConnection === true){ // connection not found -> delete connection let deleteConnection = currentConnectionData[d].connection; if(deleteConnection){ // check if "source" and "target" still exist before remove // this is NOT the case if the system was removed previous if( deleteConnection.source && deleteConnection.target ){ mapConfig.map.deleteConnection(deleteConnection, {fireEvent: false}); } } } } mapConfig.map.setSuspendDrawing(false, true); // update local connection cache updateConnectionsCache(mapConfig.map); return resolve(payload); }; /** * apply current active scope filter * @param payload * @returns {Promise} */ let filterMapByScopes = payload => new Promise(resolve => { Util.getLocalStore('map').getItem(payload.data.mapConfig.config.id).then(dataStore => { let scopes = []; if(dataStore && dataStore.filterScopes){ scopes = dataStore.filterScopes; } MapUtil.filterMapByScopes(payload.data.mapConfig.map, scopes); resolve(payload); }); }); /** * show signature overlays * @param payload * @returns {Promise} */ let showInfoSignatureOverlays = payload => new Promise(resolve => { Util.getLocalStore('map').getItem(payload.data.mapConfig.config.id).then(dataStore => { if(dataStore && dataStore.mapSignatureOverlays){ MapOverlay.showInfoSignatureOverlays($(payload.data.mapConfig.map.getContainer())); } resolve(payload); }); }); /** * after map update is complete * -> trigger update event for 'global' modules * @param payload * @returns {Promise} */ let afterUpdate = payload => new Promise(resolve => { // in rare cases there is a bug where map is undefined (hard to reproduce let map = Util.getObjVal(payload, 'data.mapConfig.map'); if(map){ let tabContentEl = map.getContainer().closest(`.${Util.config.mapTabContentClass}`); $(tabContentEl).trigger('pf:updateGlobalModules', { payload: Util.getObjVal(payload, 'data.mapConfig.config.id') }); } resolve(payload); }); return new Promise(updateMapExecutor) .then(showInfoSignatureOverlays) .then(filterMapByScopes) .then(afterUpdate); }; /** * update local connections cache (cache all connections from a map) * @param map */ let updateConnectionsCache = map => { let connections = map.getAllConnections(); let mapContainer = $(map.getContainer()); let mapId = mapContainer.data('id'); if(mapId > 0){ // clear cache connectionCache[mapId] = []; for(let i = 0; i < connections.length; i++){ updateConnectionCache(mapId, connections[i]); } }else{ console.warn('updateConnectionsCache', 'missing mapId'); } }; /** * update local connection cache (single connection) * @param mapId * @param connection */ let updateConnectionCache = (mapId, connection) => { if( mapId > 0 && connection ){ let connectionId = parseInt(connection.getParameter('connectionId')); if(connectionId > 0){ connectionCache[mapId][connectionId] = connection; } }else{ console.warn('updateConnectionCache', 'missing data'); } }; /** * get a connection object from "cache" * -> this requires the "connectionCache" cache is up2date! * @param mapId * @param connectionId * @returns {*|null} */ $.fn.getConnectionById = function(mapId, connectionId){ return Util.getObjVal(connectionCache, [mapId, connectionId].join('.')) || null; }; /** * mark a system as source * @param map * @param system */ let makeSource = (map, system) => { if(!map.isSource(system)){ // get scope from map defaults let sourceConfig = globalMapConfig.source; sourceConfig.scope = map.Defaults.Scope; // set all allowed connections for this scopes // default connector for initial dragging a new connection sourceConfig.connector = MapUtil.getScopeInfoForConnection('wh', 'connectorDefinition'); map.makeSource(system, sourceConfig); } }; /** * mark a system as target * @param map * @param system */ let makeTarget = (map, system) => { if(!map.isTarget(system)){ // get scope from map defaults let targetConfig = globalMapConfig.target; targetConfig.scope = map.Defaults.Scope; // set all allowed connections for this scopes map.makeTarget(system, targetConfig); } }; /** * checks if json system data is valid * @param systemData * @returns {boolean} */ let isValidSystem = systemData => (Util.getObjVal(systemData, 'name') || '').length > 0; /** * draw a system with its data to a map * @param map * @param systemData * @param connectedSystem * @param connectionData * @returns {Promise} */ let drawSystem = (map, systemData, connectedSystem, connectionData = null) => new Promise((resolve, reject) => { if(isValidSystem(systemData)){ let payloadDrawSystem = { action: 'drawSystem' }; let mapContainer = $(map.getContainer()); // get System Element by data let newSystem = mapContainer.getSystem(map, systemData); // add new system to map mapContainer.append(newSystem); // make new system editable makeEditable(newSystem); // make target makeTarget(map, newSystem); // make source makeSource(map, newSystem); // set system observer setSystemObserver(map, newSystem); // register system to "magnetizer" Magnetizer.addElement(systemData.mapId, newSystem[0]); payloadDrawSystem.data = { system: newSystem }; // connect new system (if connection data is given) if(connectedSystem){ // hint: "scope + type" might be changed automatically when it gets saved // -> based on jump distance,.. connectionData = Object.assign({}, { source: $(connectedSystem).data('id'), target: newSystem.data('id'), scope: map.Defaults.Scope, type: [MapUtil.getDefaultConnectionTypeByScope(map.Defaults.Scope)] }, connectionData); drawConnection(map, connectionData) .then(payload => saveConnection(payload.data.connection, Boolean(connectionData.disableAutoScope))) .then(payload => { payloadDrawSystem.data = { connection: payload.data.connection }; resolve(payloadDrawSystem); }) .catch(reject); }else{ resolve(payloadDrawSystem); } }else{ reject(new Error(`drawSystem() failed. Invalid systemData`)); } }); /** * make a system name/alias editable by x-editable * @param system */ let makeEditable = system => { system = $(system); let headElement = $(system).find('.' + config.systemHeadNameClass); headElement.editable({ mode: 'popup', type: 'text', name: 'alias', emptytext: system.data('name'), title: 'System alias', placement: 'top', onblur: 'submit', toggle: 'manual', // is triggered manually on dblClick showbuttons: false }); headElement.on('save', function(e, params){ // system alias changed -> mark system as updated MapUtil.markAsChanged(system); }); headElement.on('shown', function(e, editable){ // hide tooltip when xEditable is visible system.toggleSystemTooltip('hide', {}); let inputElement = editable.input.$input.select(); // "fake" timeout until dom rendered setTimeout(function(input){ // pre-select value input.select(); }, 0, inputElement); }); headElement.on('hidden', function(e, editable){ // show tooltip "again" on xEditable hidden system.toggleSystemTooltip('show', {show: true}); // if system with changed (e.g. long alias) -> revalidate system let map = MapUtil.getMapInstance(system.attr('data-mapid')); revalidate(map, system); }); }; /** * update z-index for a system (dragged systems should be always on top) */ $.fn.updateSystemZIndex = function(){ return this.each(function(){ // increase global counter let newZIndexSystem = config.zIndexCounter++; $(this).css('z-index', newZIndexSystem); }); }; /** * stores a connection in database * @param connection * @param disableAutoScope * @returns {Promise} */ let saveConnection = (connection, disableAutoScope = false) => new Promise((resolve, reject) => { if(!(connection instanceof jsPlumb.Connection)){ reject(new Error(`saveConnection(): connection must be instanceof jsPlumb.Connection`)); } connection.addType('state_process'); let map = connection._jsPlumb.instance; let mapContainer = $(map.getContainer()); let mapId = mapContainer.data('id'); let connectionData = MapUtil.getDataByConnection(connection); connectionData.mapId = mapId; connectionData.disableAutoScope = disableAutoScope; Util.request('PUT', 'Connection', [], connectionData, { connection: connection, map: map, mapId: mapId, oldConnectionData: connectionData }).then( payload => { let newConnectionData = payload.data; if(!$.isEmptyObject(newConnectionData)){ // update connection data e.g. "scope" has auto detected connection = updateConnection(payload.context.connection, newConnectionData); // new/updated connection should be cached immediately! updateConnectionCache(payload.context.mapId, connection); // connection scope let scope = MapUtil.getScopeInfoForConnection(newConnectionData.scope, 'label'); let title = 'New connection established'; if(payload.context.oldConnectionData.id > 0){ title = 'Connection switched'; } Util.showNotify({title: title, text: 'Scope: ' + scope, type: 'success'}); resolve({ action: 'saveConnection', data: { connection: connection } }); }else{ // some save errors payload.context.map.deleteConnection(payload.context.connection, {fireEvent: false}); reject(new Error(`saveConnection(): response error`)); } }, payload => { // remove this connection from map payload.context.map.deleteConnection(payload.context.connection, {fireEvent: false}); Util.handleAjaxErrorResponse(payload); reject(new Error(`saveConnection(): request error`)); } ); }); /** * get context menu config for a map component (e.g. system, connection,..) * @param component * @returns {Promise} */ let getContextMenuConfig = component => { if(component instanceof $ && component.hasClass(config.systemClass)){ return getSystemContextMenuConfig(component); }else if(component instanceof window.jsPlumbInstance){ return getMapContextMenuConfig(component); }else if(component instanceof jsPlumb.Connection){ return getConnectionContextMenuConfig(component); }else if(component instanceof jsPlumb.Endpoint){ return getEndpointContextMenuConfig(component); } }; /** * get context menu config for system * @param system * @returns {Promise} */ let getSystemContextMenuConfig = system => { let executor = resolve => { let options = MapContextMenu.defaultMenuOptionConfig(); options.id = MapContextMenu.config.systemContextMenuId; options.selectCallback = systemActions; let mapContainer = system.closest('.' + Util.config.mapClass); // hidden menu actions if(system.data('locked') === true){ options.hidden.push('delete_system'); } if( !mapContainer.find('.' + MapUtil.config.systemActiveClass).length){ options.hidden.push('find_route'); } // active menu actions if(system.data('locked') === true){ options.active.push('lock_system'); } if(system.data('rallyUpdated') > 0){ options.active.push('set_rally'); } // disabled menu actions if(system.hasClass(MapUtil.config.systemActiveClass)){ options.disabled.push('find_route'); } resolve(options); }; return new Promise(executor); }; /** * get context menu config for map * @param map * @returns {Promise} */ let getMapContextMenuConfig = map => { let executor = resolve => { let options = MapContextMenu.defaultMenuOptionConfig(); options.id = MapContextMenu.config.mapContextMenuId; options.selectCallback = mapActions; let mapContainer = $(map.getContainer()); // active menu actions Util.getLocalStore('map').getItem(mapContainer.data('id')).then(dataStore => { if(dataStore && dataStore.filterScopes){ options.active = dataStore.filterScopes.map(scope => 'filter_' + scope); } resolve(options); }); }; return new Promise(executor); }; /** * get context menu config for connection * @param connection * @returns {Promise} */ let getConnectionContextMenuConfig = connection => { let executor = resolve => { let options = MapContextMenu.defaultMenuOptionConfig(); options.id = MapContextMenu.config.connectionContextMenuId; options.selectCallback = connectionActions; let scope = connection.scope; // hidden menu actions if(scope === 'abyssal'){ options.hidden.push('wh_eol'); options.hidden.push('preserve_mass'); options.hidden.push('change_status'); options.hidden.push('wh_jump_mass_change'); options.hidden.push('change_scope'); options.hidden.push('separator'); }else if(scope === 'stargate'){ options.hidden.push('wh_eol'); options.hidden.push('preserve_mass'); options.hidden.push('change_status'); options.hidden.push('wh_jump_mass_change'); options.hidden.push('scope_stargate'); }else if(scope === 'jumpbridge'){ options.hidden.push('wh_eol'); options.hidden.push('preserve_mass'); options.hidden.push('change_status'); options.hidden.push('wh_jump_mass_change'); options.hidden.push('scope_jumpbridge'); }else if(scope === 'wh'){ options.hidden.push('scope_wh'); } // active menu actions if(connection.hasType('wh_eol') === true){ options.active.push('wh_eol'); } if(connection.hasType('preserve_mass') === true){ options.active.push('preserve_mass'); } for(let sizeName of Object.keys(Init.wormholeSizes)){ if(connection.hasType(sizeName)){ options.active.push(sizeName); } } if(connection.hasType('wh_reduced') === true){ options.active.push('status_reduced'); }else if(connection.hasType('wh_critical') === true){ options.active.push('status_critical'); }else{ // not reduced is default options.active.push('status_fresh'); } // disabled menu actions if(connection.getParameter('sizeLocked')){ options.disabled.push('wh_jump_mass_change'); } resolve(options); }; return new Promise(executor); }; /** * get context menu config for endpoint * @param endpoint * @returns {Promise} */ let getEndpointContextMenuConfig = endpoint => { let executor = resolve => { let options = MapContextMenu.defaultMenuOptionConfig(); options.id = MapContextMenu.config.endpointContextMenuId; options.selectCallback = endpointActions; // active menu actions if(endpoint.hasType('bubble') === true){ options.active.push('bubble'); } resolve(options); }; return new Promise(executor); }; /** * set up all actions that can be preformed on a system * @param map * @param system */ let setSystemObserver = (map, system) => { system = $(system); // get map container let mapContainer = $(map.getContainer()); let grid = [MapUtil.config.mapSnapToGridDimension, MapUtil.config.mapSnapToGridDimension]; // map overlay will be set on "drag" start let mapOverlayTimer = null; let debounceDrag = false; // make system draggable map.draggable(system, { containment: 'parent', constrain: true, //scroll: true, // not working because of customized scrollbar filter: filterSystemHeadEvent, snapThreshold: MapUtil.config.mapSnapToGridDimension, // distance for grid snapping "magnet" effect (optional) start: function(params){ let dragSystem = $(params.el); dragSystem.css('pointer-events','none'); mapOverlayTimer = MapOverlayUtil.getMapOverlay(dragSystem, 'timer'); // start map update timer mapOverlayTimer.startMapUpdateCounter(); // check if grid-snap is enable -> this enables napping for !CURRENT! Element if(mapContainer.hasClass(MapUtil.config.mapGridClass)){ params.drag.params.grid = grid; }else{ delete( params.drag.params.grid ); } // drag system is not always selected let selectedSystems = mapContainer.getSelectedSystems().get(); selectedSystems = selectedSystems.concat(dragSystem.get()); selectedSystems = $.unique( selectedSystems ); // hide tooltip $(selectedSystems).toggleSystemTooltip('hide', {}); // destroy popovers $(selectedSystems).destroyPopover(true); // move them to the "top" $(selectedSystems).updateSystemZIndex(); }, drag: function(p){ if(!debounceDrag) { requestAnimationFrame(() => { // start map update timer mapOverlayTimer.startMapUpdateCounter(); // update system positions for "all" systems that are effected by drag&drop // this requires "magnet" feature to be active! (optional) Magnetizer.executeAtEvent(map, p.e); debounceDrag = false; }); } debounceDrag = true; }, stop: function(params){ let dragSystem = $(params.el); // start map update timer mapOverlayTimer.startMapUpdateCounter(); // show tooltip dragSystem.toggleSystemTooltip('show', {show: true}); // mark as "changed" MapUtil.markAsChanged(dragSystem); // set new position for popover edit field (system name) let newPosition = dragSystem.position(); let placement = 'top'; if(newPosition.top < 100){ placement = 'bottom'; } if(newPosition.left < 100){ placement = 'right'; } dragSystem.find('.' + config.systemHeadNameClass).editable('option', 'placement', placement); // update all dragged systems -> added to DragSelection params.selection.forEach(elData => { MapUtil.markAsChanged($(elData[0]).css('pointer-events','initial')); }); } }); if(system.data('locked') === true){ map.setDraggable(system, false); } // init system tooltips ======================================================================================= let systemTooltipOptions = { toggle: 'tooltip', placement: 'right', viewport: system.id }; //system.find('.fas').tooltip(systemTooltipOptions); // system click events ======================================================================================== let double = function(e){ let system = $(this); let headElement = $(system).find('.' + config.systemHeadNameClass); // update z-index for system, editable field should be on top // move them to the "top" $(system).updateSystemZIndex(); // show "set alias" input (x-editable) headElement.editable('show'); }; let single = function(e){ // check if click was performed on "popover" (x-editable) let popoverClick = false; if( $(e.target).closest('.popover').length ){ popoverClick = true; } // continue if click was *not* on a popover dialog of a system if(!popoverClick){ let system = $(this); // left mouse button if(e.which === 1){ if(e.ctrlKey === true){ // select system MapUtil.toggleSystemsSelect(map, [system]); }else{ MapUtil.showSystemInfo(map, system); } } } }; Util.singleDoubleClick(system[0], single, double); }; /** * callback after system save * @param map * @param newSystemData * @param sourceSystem * @param connectionData * @returns {Promise} */ let saveSystemCallback = (map, newSystemData, sourceSystem, connectionData = null) => drawSystem(map, newSystemData, sourceSystem, connectionData); /** * select all (selectable) systems on a mapElement */ $.fn.selectAllSystems = function(){ return this.each(function(){ let mapElement = $(this); let map = getMapInstance(mapElement.data('id')); let allSystems = mapElement.find('.' + config.systemClass + ':not(.' + config.systemSelectedClass + ')' + ':not(.' + MapUtil.config.systemHiddenClass + ')' ); // filter non-locked systems allSystems = allSystems.filter(function(i, el){ return ( $(el).data('locked') !== true ); }); MapUtil.toggleSystemsSelect(map, allSystems); Util.showNotify({title: allSystems.length + ' systems selected', type: 'success'}); }); }; /** * toggle log status of a system * @param poke * @param options */ $.fn.toggleLockSystem = function(poke, options){ let system = $(this); let map = options.map; let hideNotification = false; if(options.hideNotification === true){ hideNotification = true; } let hideCounter = false; if(options.hideCounter === true){ hideCounter = true; } let systemName = system.getSystemInfo( ['alias'] ); if( system.data('locked') === true ){ system.data('locked', false); system.removeClass(MapUtil.config.systemLockedClass); // enable draggable map.setDraggable(system, true); if(! hideNotification){ Util.showNotify({title: 'System unlocked', text: systemName, type: 'unlock'}); } }else{ system.data('locked', true); system.addClass(MapUtil.config.systemLockedClass); // enable draggable map.setDraggable(system, false); if(! hideNotification){ Util.showNotify({title: 'System locked', text: systemName, type: 'lock'}); } } // repaint connections revalidate(map, system); if(!hideCounter){ MapOverlayUtil.getMapOverlay(system, 'timer').startMapUpdateCounter(); } }; /** * get a new jsPlumb map instance or or get a cached one for update * @param mapId * @returns {*} */ let getMapInstance = function(mapId){ if(!MapUtil.existsMapInstance(mapId)){ // create new instance jsPlumb.Defaults.LogEnabled = true; let newJsPlumbInstance = jsPlumb.getInstance({ Anchor: ['Continuous', {faces: ['top', 'right', 'bottom', 'left']}], // single anchor (used during drag action) Anchors: [ ['Continuous', {faces: ['top', 'right', 'bottom', 'left']}], ['Continuous', {faces: ['top', 'right', 'bottom', 'left']}], ], Container: null, // will be set as soon as container is connected to DOM PaintStyle: { strokeWidth: 4, // connection width (inner) stroke: '#3c3f41', // connection color (inner) outlineWidth: 2, // connection width (outer) outlineStroke: '#63676a', // connection color (outer) dashstyle: '0', // connection dashstyle (default) -> is used after connectionType got removed that has dashstyle specified 'stroke-linecap': 'round' // connection shape }, Endpoint: ['Dot', {radius: 5}], // single endpoint (used during drag action) Endpoints: [ ['Dot', {radius: 5, cssClass: config.endpointSourceClass}], ['Dot', {radius: 5, cssClass: config.endpointTargetClass}] ], EndpointStyle: {fill: '#3c3f41', stroke: '#63676a', strokeWidth: 2}, // single endpoint style (used during drag action) EndpointStyles: [ {fill: '#3c3f41', stroke: '#63676a', strokeWidth: 2}, {fill: '#3c3f41', stroke: '#63676a', strokeWidth: 2} ], Connector: ['Bezier', {curviness: 40}], // default connector style (this is not used!) all connections have their own style (by scope) ReattachConnections: false, // re-attach connection if dragged with mouse to "nowhere" Scope: Init.defaultMapScope, // default map scope for connections LogEnabled: true }); // register all available endpoint types newJsPlumbInstance.registerEndpointTypes(globalMapConfig.endpointTypes); // register all available connection types newJsPlumbInstance.registerConnectionTypes(globalMapConfig.connectionTypes); // ======================================================================================================== // Event Interceptors https://community.jsplumbtoolkit.com/doc/interceptors.html //========================================================================================================= // This is called when a new or existing connection has been dropped // If you return false (or nothing) from this callback, the new Connection is aborted and removed from the UI. newJsPlumbInstance.bind('beforeDrop', function(info){ let connection = info.connection; let dropEndpoint = info.dropEndpoint; let sourceId = info.sourceId; let targetId = info.targetId; // loop connection not allowed if(sourceId === targetId){ console.warn('Source/Target systems are identical'); return false; } // connection can not be dropped on an endpoint that already has other connections on it if(dropEndpoint.connections.length > 0){ console.warn('Endpoint already occupied'); return false; } let sourceSystem = $('#' + sourceId); let targetSystem = $('#' + targetId); // switch connection type to "abyss" in case source OR target system belongs to "a-space" if(sourceSystem.data('typeId') === 3 || targetSystem.data('typeId') === 3){ setConnectionScope(connection, 'abyssal'); } // set "default" connection status only for NEW connections if(!connection.suspendedElement){ MapUtil.addConnectionTypes(connection, [MapUtil.getDefaultConnectionTypeByScope(connection.scope)]); } // prevent multiple connections between same systems let connections = MapUtil.checkForConnection(newJsPlumbInstance, sourceId, targetId); if(connections.length > 1){ bootbox.confirm('Connection already exists. Do you really want to add an additional one?', result => { if(!result && connection._jsPlumb){ // connection._jsPlumb might be "undefined" in case connection was removed in the meantime connection._jsPlumb.instance.detach(connection); } }); } // always save the new connection saveConnection(connection).catch(console.warn); return true; }); // This is called when the user starts to drag an existing Connection. // Returning false from beforeStartDetach prevents the Connection from being dragged. newJsPlumbInstance.bind('beforeStartDetach', function(info){ return true; }); // This is called when the user has detached a Connection, which can happen for a number of reasons: // by default, jsPlumb allows users to drag Connections off of target Endpoints, but this can also result from a programmatic 'detach' call. newJsPlumbInstance.bind('beforeDetach', function(connection){ return true; }); // ======================================================================================================== // Events https://community.jsplumbtoolkit.com/doc/events.html //========================================================================================================= // Notification a Connection was established. // Note: jsPlumb.connect causes this event to be fired, but there is of course no original event when a connection is established programmatically. newJsPlumbInstance.bind('connection', function(info, e){ }); // Notification a Connection or Endpoints was clicked. newJsPlumbInstance.bind('click', function(component, e){ if(component instanceof jsPlumb.Connection){ connectionClickHandler(component,e); } }); // Notification that an existing connection's source or target endpoint was dragged to some new location. newJsPlumbInstance.bind('connectionMoved', function(info, e){ }); // Notification an existing Connection is being dragged. // Note that when this event fires for a brand new Connection, the target of the Connection is a transient element // that jsPlumb is using for dragging, and will be removed from the DOM when the Connection is subsequently either established or aborted. newJsPlumbInstance.bind('connectionDrag', function(info, e){ }); // Notification a Connection was detached. // In the event that the Connection was new and had never been established between two Endpoints, it has a pending flag set on it. newJsPlumbInstance.bind('connectionDetached', function(info, e){ // a connection is manually (drag&drop) detached! otherwise this event should not be send! let connection = info.connection; MapUtil.deleteConnections([connection]); }); // Right-click on some given component. jsPlumb will report right clicks on both Connections and Endpoints. newJsPlumbInstance.bind('contextmenu', function(component, e){ getContextMenuConfig(component).then(payload => { let context = { component: component }; MapContextMenu.openMenu(payload, e, context); }); }); // Notification the current zoom was changed newJsPlumbInstance.bind('zoom', function(zoom){ MapOverlay.updateZoomOverlay(this); // store new zoom level in IndexDB if(zoom === 1){ Util.getLocalStore('map').removeItem(`${mapId}.mapZoom`); }else{ Util.getLocalStore('map').setItem(`${mapId}.mapZoom`, zoom); } }); // ======================================================================================================== // Events for interactive CSS classes https://community.jsplumbtoolkit.com/doc/styling-via-css.html //========================================================================================================= // This event is responsible for dynamic CSS classes "_jsPlumb_target_hover", "_jsPlumb_drag_select" newJsPlumbInstance.bind('checkDropAllowed', function(params){ let sourceEndpoint = params.sourceEndpoint; let targetEndpoint = params.targetEndpoint; // connections can not be attached to foreign endpoints // the only endpoint available is the endpoint from where the connection was dragged away (re-attach) return (targetEndpoint.connections.length === 0); }); MapUtil.setMapInstance(mapId, newJsPlumbInstance); } return MapUtil.getMapInstance(mapId); }; /** * check if there is an focus() element found as parent of tabContentElement * -> or if there is any other active UI element found (e.g. dialog, xEditable, Summernote) * @param tabContentElement * @returns {*} */ let systemFormsActive = (tabContentElement) => { let activeNode = null; if(tabContentElement.length){ // tabContentElement exists ... tabContentElement = tabContentElement[0]; // ... check for current active/focus() element and is not the default element ... if( Util.isDomElement(document.activeElement) && document.activeElement !== document.body ){ let activeElementTagName = document.activeElement.tagName.toLocaleLowerCase(); // ... check for active form elements ... let isFormElement = ['input', 'select', 'textarea'].includes(activeElementTagName); let isChildElement = tabContentElement.contains(document.activeElement); if(isFormElement && isChildElement){ activeNode = activeElementTagName; }else{ // ... check for open dialogs/xEditable elements ... if(Util.isDomElement(document.querySelector('.bootbox'))){ activeNode = 'dialogOpen'; }else if(Util.isDomElement(document.querySelector('.editable-open'))){ activeNode = 'xEditableOpen'; }else{ // ... check for open Summernote editor let summernoteElement = tabContentElement.querySelector('.' + Util.config.summernoteClass); if( Util.isDomElement(summernoteElement) && typeof $(summernoteElement).data().summernote === 'object' ){ activeNode = 'SummernoteOpen'; } } } } } return activeNode; }; /** * set observer for a map container * @param map */ let setMapObserver = map => { // get map container let mapContainer = $(map.getContainer()); MapOverlay.initMapDebugOverlays(map); // context menu for mapContainer mapContainer.on('contextmenu', function(e){ e.preventDefault(); e.stopPropagation(); // make sure map is clicked and NOT a connection if($(e.target).hasClass(Util.config.mapClass)){ getContextMenuConfig(map).then(payload => { let context = { component: map }; MapContextMenu.openMenu(payload, e, context); }); } }); // context menu for systems mapContainer.on('contextmenu', '.' + config.systemClass, function(e){ e.preventDefault(); e.stopPropagation(); let systemElement = $(e.currentTarget); getContextMenuConfig(systemElement).then(payload => { let context = { component: systemElement }; MapContextMenu.openMenu(payload, e, context); }); }); // init drag-frame selection ---------------------------------------------------------------------------------- let dragSelect = new DragSelect({ target: mapContainer[0], selectables: '.' + config.systemClass + ':not(.' + MapUtil.config.systemLockedClass + '):not(.' + MapUtil.config.systemHiddenClass + ')', selectedClass: MapUtil.config.systemSelectedClass, selectBoxClass: 'pf-map-drag-to-select', boundary: '.mCSB_container_wrapper', onShow: () => { Util.triggerMenuAction(document, 'Close'); }, onHide: (deselectedSystems) => { let selectedSystems = mapContainer.getSelectedSystems(); if(selectedSystems.length > 0){ // make all selected systems draggable Util.showNotify({title: selectedSystems.length + ' systems selected', type: 'success'}); // convert former group draggable systems so single draggable for(let i = 0; i < selectedSystems.length; i++){ map.addToDragSelection(selectedSystems[i]); } } // convert former group draggable systems so single draggable for(let i = 0; i < deselectedSystems.length; i++){ map.removeFromDragSelection(deselectedSystems[i]); } }, debug: false, debugEvents: false }); // system body expand ----------------------------------------------------------------------------------------- mapContainer.hoverIntent({ over: function(e){ let system = $(this).closest('.' + config.systemClass); let map = MapUtil.getMapInstance(system.attr('data-mapid')); let systemId = system.attr('id'); let systemBody = system.find('.' + config.systemBodyClass); // bring system in front (increase zIndex) system.updateSystemZIndex(); // get ship counter and calculate expand height let userCount = parseInt(system.data('userCount')); let expandHeight = userCount * config.systemBodyItemHeight; // calculate width let width = system[0].clientWidth; let minWidth = 150; let newWidth = width > minWidth ? width : minWidth; // in case of big systems systemBody.velocity('stop').velocity( { height: expandHeight + 'px', width: newWidth, 'min-width': minWidth + 'px' },{ easing: 'easeOut', duration: 60, progress: function(){ // repaint connections of current system map.revalidate(systemId); }, complete: function(){ map.revalidate(systemId); // extend player name element let systemBody = $(this); let systemBodyItemNameWidth = newWidth - 50 - 10 - 20; // - bodyRight - icon - somePadding systemBody.find('.' + config.systemBodyItemNameClass).css({width: systemBodyItemNameWidth + 'px'}); systemBody.find('.' + config.systemBodyRightClass).show(); } } ); }, out: function(e){ let system = $(this).closest('.' + config.systemClass); let map = MapUtil.getMapInstance(system.attr('data-mapid')); let systemId = system.attr('id'); let systemBody = system.find('.' + config.systemBodyClass); // stop animation (prevent visual bug if user spams hover-icon [in - out]) systemBody.velocity('stop'); // reduce player name element back to "normal" size (css class width is used) systemBody.find('.' + config.systemBodyRightClass).hide(); systemBody.find('.' + config.systemBodyItemNameClass).css({width: ''}); systemBody.velocity('reverse', { complete: function(){ // overwrite "complete" function from first "hover"-open // set animated "with" back to default "100%" important in case of system with change (e.g. longer name) $(this).css({width: ''}); map.revalidate(systemId); } }); }, selector: '.' + config.systemClass + ' .' + config.systemHeadExpandClass }); mapContainer.hoverIntent({ over: function(e){ $(this).tooltip({ trigger: 'manual', placement: 'right', viewport: this.closest(`.${config.systemClass}`) }).tooltip('show'); }, out: function(e){ $(this).tooltip('destroy'); }, selector: `.${config.systemClass} .fas[title]` }); // system "active users" popover ------------------------------------------------------------------------------ mapContainer.hoverIntent({ over: function(e){ let counterElement = $(this); let systemElement = counterElement.closest('.' + config.systemClass); let mapId = systemElement.data('mapid'); let systemId = systemElement.data('systemId'); let userData = Util.getCurrentMapUserData(mapId); let systemUserData = Util.getCharacterDataBySystemId(userData.data.systems, systemId); counterElement.addSystemPilotTooltip(systemUserData, { trigger: 'manual', placement: 'right' }).setPopoverSmall().popover('show'); }, out: function(e){ $(this).destroyPopover(); }, selector: '.' + config.systemHeadCounterClass }); // system "effect" popover ------------------------------------------------------------------------------------ // -> event delegation to system elements, popup only if needed (hover) mapContainer.hoverIntent({ over: function(e){ let effectElement = $(this); let systemElement = effectElement.closest('.' + config.systemClass); let security = systemElement.data('security'); let effect = systemElement.data('effect'); effectElement.addSystemEffectTooltip(security, effect, { trigger: 'manual', placement: 'right' }).setPopoverSmall().popover('show'); }, out: function(e){ $(this).destroyPopover(); }, selector: '.' + config.systemClass + ' .' + MapUtil.getEffectInfoForSystem('effect', 'class') }); // system "statics" popover ----------------------------------------------------------------------------------- // -> event delegation to system elements, popup only if needed (hover) MapUtil.initWormholeInfoTooltip( mapContainer, '.' + config.systemHeadInfoClass + ' span[class^="pf-system-sec-"]', {placement: 'right', smaller: true} ); // toggle "fullSize" Endpoint overlays for system (signature information) ------------------------------------- mapContainer.hoverIntent({ over: function(e){ for(let overlayInfo of map.selectEndpoints({element: this}).getOverlay(MapOverlayUtil.config.endpointOverlayId)){ if(overlayInfo[0] instanceof jsPlumb.Overlays.Label){ overlayInfo[0].fire('toggleSize', true); } } }, out: function(e){ for(let overlayInfo of map.selectEndpoints({element: this}).getOverlay(MapOverlayUtil.config.endpointOverlayId)){ if(overlayInfo[0] instanceof jsPlumb.Overlays.Label){ overlayInfo[0].fire('toggleSize', false); } } }, selector: '.' + config.systemClass }); // catch events =============================================================================================== /** * update/toggle global map option (e.g. "grid snap", "magnetization") * @param mapContainer * @param data */ let updateMapOption = (mapContainer, data) => { // get map menu config options let mapOption = mapOptions[data.option]; Util.getLocalStore('map').getItem(mapContainer.data('id')).then(function(dataStore){ let notificationText = 'disabled'; let button = $('#' + this.mapOption.buttonId); let dataExists = false; if( dataStore && dataStore[this.data.option] ){ dataExists = true; } if(dataExists === this.data.toggle){ // toggle button class button.removeClass('active'); // toggle map class (e.g. for grid) if(this.mapOption.class){ this.mapContainer.removeClass(MapUtil.config[this.mapOption.class]); } // call optional jQuery extension on mapContainer if(this.mapOption.onDisable && !this.data.skipOnDisable){ this.mapOption.onDisable(this.mapContainer); } // show map overlay info icon MapOverlayUtil.getMapOverlay(this.mapContainer, 'info').updateOverlayIcon(this.data.option, 'hide'); // delete map option Util.getLocalStore('map').removeItem(`${this.mapContainer.data('id')}.${this.data.option}`); }else{ // toggle button class button.addClass('active'); // toggle map class (e.g. for grid) if(this.mapOption.class){ this.mapContainer.addClass(MapUtil.config[this.mapOption.class]); } // call optional jQuery extension on mapContainer if(this.mapOption.onEnable && !this.data.skipOnEnable){ this.mapOption.onEnable(this.mapContainer); } // hide map overlay info icon MapOverlayUtil.getMapOverlay(this.mapContainer, 'info').updateOverlayIcon(this.data.option, 'show'); // store map option Util.getLocalStore('map').setItem(`${this.mapContainer.data('id')}.${this.data.option}`, 1); notificationText = 'enabled'; } if(this.data.toggle){ Util.showNotify({title: this.mapOption.description, text: notificationText, type: 'info'}); } }.bind({ data: data, mapOption: mapOption, mapContainer: mapContainer })); }; /** * select system event * @param mapContainer * @param data */ let selectSystem = (mapContainer, data) => { let systemId = MapUtil.getSystemId(mapContainer.data('id'), data.systemId); let system = mapContainer.find('#' + systemId); if(system.length === 1){ // system found on map ... let select = Util.getObjVal(data, 'forceSelect') !== false; if(!select){ // ... select is NOT "forced" -> auto select system on jump let activeElement = systemFormsActive(MapUtil.getTabContentElementByMapElement(system)); if(activeElement !== null){ console.info('Skip auto select systemId %i. Reason: %o', data.systemId, activeElement); }else{ select = true; } } if(select){ let areaMap = mapContainer.closest('.' + Util.getMapTabContentAreaClass('map')); Scrollbar.scrollToCenter(areaMap, system); // select system MapUtil.showSystemInfo(map, system); } } }; mapContainer.on('pf:menuAction', (e, action, data) => { // menuAction events can also be triggered on child nodes // -> if event is not handled there it bubbles up // make sure event can be handled by this element if(e.target === e.currentTarget){ e.stopPropagation(); switch(action){ case 'MapOption': // toggle global map option (e.g. "grid snap", "magnetization") updateMapOption(mapContainer, data); break; case 'SelectSystem': // select system on map (e.g. from modal links) selectSystem(mapContainer, data); break; case 'AddSystem': System.showNewSystemDialog(map, data, typeof data.callback === 'function' ? data.callback : saveSystemCallback); break; default: console.warn('Unknown menuAction %o event name', action); } }else{ console.warn('Unhandled menuAction %o event name. Handled menu events should not bobble up', action); } }); // delete system event // triggered from "map info" dialog scope mapContainer.on('pf:deleteSystems', function(e, data){ System.deleteSystems(map, data.systems, data.callback); }); // triggered when map lock timer (interval) was cleared mapContainer.on('pf:unlocked', function(){ let mapElement = $(this); let mapId = mapElement.data('id'); // check if there was a mapUpdate during map was locked let mapQueueIndex = mapUpdateQueue.indexOf(mapId); if(mapQueueIndex !== -1){ // get current mapConfig let mapConfig = Util.getCurrentMapData(mapId); if(mapConfig){ // map data is available => update map updateMap(mapConfig); } // update done -> clear mapId from mapUpdateQueue mapUpdateQueue.splice(mapQueueIndex, 1); } }); // update "local" overlay for this map mapContainer.on('pf:updateLocal', function(e, userData){ let mapId = Util.getObjVal(userData, 'config.id') || 0; if(mapId){ let mapElement = $(this); let mapOverlay = MapOverlayUtil.getMapOverlay(mapElement, 'local'); let currentMapData = Util.getCurrentMapData(mapId); let currentCharacterLog = Util.getCurrentCharacterData('log'); let clearLocal = true; if( currentMapData && currentCharacterLog && currentCharacterLog.system ){ let currentSystemData = currentMapData.data.systems.filter(system => { return system.systemId === currentCharacterLog.system.id; }); if(currentSystemData.length){ // current user system is on this map currentSystemData = currentSystemData[0]; // check for active users "nearby" (x jumps radius) let nearBySystemData = Util.getNearBySystemData(currentSystemData, currentMapData, MapUtil.config.defaultLocalJumpRadius); let nearByCharacterData = Util.getNearByCharacterData(nearBySystemData, userData.data.systems); // update "local" table in overlay mapOverlay.updateLocalTable(currentSystemData, nearByCharacterData); clearLocal = false; } } if(clearLocal){ mapOverlay.clearLocalTable(); } } }); }; /** * get system data out of its object * @param info * @returns {*} */ $.fn.getSystemInfo = function(info){ let systemInfo = []; for(let i = 0; i < info.length; i++){ switch(info[i]){ case 'alias': // get current system alias let systemHeadNameElement = $(this).find('.' + config.systemHeadNameClass); let alias = ''; if(systemHeadNameElement.hasClass('editable')){ // xEditable is initiated alias = systemHeadNameElement.editable('getValue', true); } systemInfo.push(alias ); break; default: systemInfo.push('bad system query'); } } if(systemInfo.length === 1){ return systemInfo[0]; }else{ return systemInfo; } }; /** * updates all systems on map with current user Data (all users on this map) * update the Data of the user that is currently viewing the map (if available) * @param mapElement * @param userData * @returns {Promise} */ let updateUserData = (mapElement, userData) => { let updateUserDataExecutor = (resolve, reject) => { let payload = { action: 'updateUserData' }; // get new map instance or load existing let map = getMapInstance(userData.config.id); let mapElement = map.getContainer(); // container must exist! otherwise systems can not be updated if(mapElement !== undefined){ mapElement = $(mapElement); // no user update for 'frozen' maps... if(mapElement.data('frozen') === true){ return resolve(payload); } // compact/small system layout or not let compactView = mapElement.hasClass(MapUtil.config.mapCompactClass); // get current character log data let characterLogSystemId = Util.getObjVal(Util.getCurrentCharacterData('log'), 'system.id') || 0; // data for header update let headerUpdateData = { mapId: userData.config.id, userCountInside: 0, // active user on a map userCountOutside: 0, // active user NOT on map userCountInactive: 0 }; // check if current user was found on the map let currentUserOnMap = false; // get all systems for(let system of mapElement.find('.' + config.systemClass)){ system = $(system); let systemId = system.data('systemId'); let tempUserData = null; // check if user is currently in "this" system let currentUserIsHere = false; let j = userData.data.systems.length; // search backwards to avoid decrement the counter after splice() while(j--){ let systemData = userData.data.systems[j]; // check if any user is in this system if(systemId === systemData.id){ tempUserData = systemData; // add "user count" to "total map user count" headerUpdateData.userCountInside += tempUserData.user.length; // remove system from "search" array -> speed up loop userData.data.systems.splice(j, 1); } } // the current user can only be in a single system ------------------------------------------------ if( !currentUserOnMap && characterLogSystemId && characterLogSystemId === systemId ){ currentUserIsHere = true; currentUserOnMap = true; } updateSystemUserData(map, system, tempUserData, currentUserIsHere, {compactView: compactView}); } // users who are not in any map system ---------------------------------------------------------------- for(let systemData of userData.data.systems){ // users without location are grouped in systemId: 0 if(systemData.id){ headerUpdateData.userCountOutside += systemData.user.length; }else{ headerUpdateData.userCountInactive += systemData.user.length; } } // trigger document event -> update header $(document).trigger('pf:updateHeaderMapData', headerUpdateData); } resolve(payload); }; return new Promise(updateUserDataExecutor); }; /** * collect all map data from client for server or client sync * @param {HTMLElement} mapContainer * @param filter * @param minimal * @returns {boolean|{}} */ let getMapDataForSync = (mapContainer, filter = [], minimal = false) => { let mapData = false; // check if there is an active map counter that prevents collecting map data (locked map) if(!MapOverlayUtil.isMapCounterOverlayActive(mapContainer)){ mapData = $(mapContainer).getMapDataFromClient(filter, minimal); } return mapData; }; /** * collect all map data for export/save for a map * this function returns the "client" data NOT the "server" data for a map * @param filter * @param minimal * @returns {{}} */ $.fn.getMapDataFromClient = function(filter = [], minimal = false){ let mapContainer = $(this); let map = getMapInstance(mapContainer.data('id')); let filterHasId = filter.includes('hasId'); let filterHasChanged = filter.includes('hasChanged'); let mapData = {}; // map config ------------------------------------------------------------------------------------------------- mapData.config = { id: parseInt(mapContainer.data('id')), name: mapContainer.data('name'), scope: { id: parseInt(mapContainer.data('scopeId')) }, icon: mapContainer.data('icon'), type: { id: parseInt(mapContainer.data('typeId')) }, created: parseInt(mapContainer.data('created')), updated: parseInt(mapContainer.data('updated')) }; let data = {}; // systems data ----------------------------------------------------------------------------------------------- let systemsData = []; let systems = mapContainer.getSystems(); for(let i = 0; i < systems.length; i++){ let system = $(systems[i]); if(filterHasChanged && !MapUtil.hasChanged(system)){ continue; } systemsData.push(system.getSystemData(minimal)); } data.systems = systemsData; // connections ------------------------------------------------------------------------------------------------ let connectionsData = []; let connections = map.getAllConnections(); for(let j = 0; j < connections.length; j++) { let connection = connections[j]; // add to cache updateConnectionCache(mapData.config.id, connection); if(filterHasChanged && !MapUtil.hasChanged(connection)){ continue; } if(filterHasId && !connection.getParameter('connectionId')){ continue; } connectionsData.push(MapUtil.getDataByConnection(connection, minimal)); } data.connections = connectionsData; mapData.data = data; return mapData; }; /** * get all relevant data for a system object * @param minimal * @returns {{id: number, updated: {updated: number}}} */ $.fn.getSystemData = function(minimal = false){ let system = $(this); let data = system.data(); let systemData = { id: parseInt(data.id), updated: { updated: parseInt(data.updated) } }; if(!minimal){ let systemDataComplete = { systemId: parseInt(data.systemId), name: data.name, alias: system.getSystemInfo(['alias']), effect: data.effect, type: { id: data.typeId }, security: data.security, trueSec: data.trueSec, region: { id: data.regionId, name: data.region }, constellation: { id: data.constellationId, name: data.constellation }, status: { id: data.statusId }, locked: data.locked ? 1 : 0, rallyUpdated: data.rallyUpdated || 0, rallyPoke: data.rallyPoke ? 1 : 0, currentUser: data.currentUser, // if user is currently in this system planets: data.planets, shattered: data.shattered ? 1 : 0, drifter: data.drifter ? 1 : 0, statics: data.statics, userCount: parseInt(data.userCount) || 0, position: MapUtil.getSystemPosition(system) }; let optionalDataKeys = ['sovereignty']; for(let dataKey of optionalDataKeys){ let value = system.data(dataKey); if(value !== null && value !== undefined){ systemDataComplete[dataKey] = value; } } systemData = Object.assign(systemData, systemDataComplete); } return systemData; }; /** * init map options * @param mapConfig * @param options * @returns {Promise} */ let initMapOptions = (mapConfig, options) => new Promise((resolve, reject) => { let payload = { action: 'initMapOptions', data: { mapConfig: mapConfig } }; if(options.showAnimation){ let mapElement = $(mapConfig.map.getContainer()); MapUtil.setMapDefaultOptions(mapElement, mapConfig.config) .then(payload => MapUtil.visualizeMap(mapElement, 'show')) .then(payload => MapUtil.zoomToDefaultScale(mapConfig.map)) .then(payload => MapUtil.scrollToDefaultPosition(mapConfig.map)) .then(payload => { Util.showNotify({title: 'Map initialized', text: mapConfig.config.name + ' - loaded', type: 'success'}); }) .then(() => resolve(payload)); }else{ // nothing to do here... resolve(payload); } }); /** * load OR updates system map * @param areaMap parent element where the map will be loaded * @param mapConfig * @param options * @returns {Promise} */ let loadMap = (areaMap, mapConfig, options) => { // whether map gets loaded (initialized) for the first time // or just updated an existing map let isFirstLoad = false; /** * load map promise * @param resolve * @param reject */ let loadMapExecutor = (resolve, reject) => { // init jsPlumb jsPlumb.ready(() => { // get new map instance or load existing mapConfig.map = getMapInstance(mapConfig.config.id); if(mapConfig.map.getContainer() === undefined){ // map not loaded -> create & update isFirstLoad = true; newMapElement(areaMap, mapConfig) .then(payload => updateMap(payload.data.mapConfig)) .then(payload => resolve(payload)); }else{ // map exists -> update updateMap(mapConfig) .then(payload => resolve(payload)); } }); }; return new Promise(loadMapExecutor) .then(payload => initMapOptions(payload.data.mapConfig, options)) .then(payload => ({ action: 'loadMap', data: payload.data, isFirstLoad })); }; /** * init scrollbar for Map element * @param areaMap */ let initMapScrollbar = areaMap => { let mapElement = areaMap.find('.' + Util.config.mapClass); let mapId = mapElement.data('id'); let dragSelect; Scrollbar.initScrollbar(areaMap, { callbacks: { onInit: function(){ let scrollWrapper = this; // ++++++++++++++++++++++++++++++++++++++++++++++++++ EventHandler.addEventListener(this, 'update:dragSelect', function(e){ e.stopPropagation(); dragSelect = e.detail; let intersection = dragSelect.getIntersection(); let originData = dragSelect._selectBox.dataset.origin; let dragOrigin = originData ? originData.split('|', 2) : []; let position = [null, null]; let inverseDirection = (directions, i) => directions[((i + 2) % directions.length + directions.length) % directions.length]; let allDirections = ['top', 'right', 'bottom', 'left']; allDirections.forEach((direction, i, allDirections) => { if(dragOrigin.includes(direction) && intersection.includes(direction)){ position[i % 2] = direction; }else if(dragOrigin.includes(direction) && intersection.includes(inverseDirection(allDirections, i))){ // reverse scroll (e.g. 1. drag&select scroll bottom end then move back to top) position[i % 2] = inverseDirection(allDirections, i); } }); Scrollbar.autoScroll(scrollWrapper, position); }, {capture: true}); // init 'space' key + 'mouse' down for map scroll ------------------------------------------------- let scrollStart = [0, 0]; let mouseStart = [0, 0]; let mouseOffset = [0, 0]; let animationFrameId = 0; let toggleDragScroll = active => { mapElement.toggleClass('disabled', active).toggleClass('pf-map-move', active); }; let stopDragScroll = () => { cancelAnimationFrame(animationFrameId); animationFrameId = 0; scrollStart = [0, 0]; mouseStart = [0, 0]; mouseOffset = [0, 0]; }; let dragScroll = () => { if(Key.isActive(' ')){ let scrollOffset = [ Math.max(0, scrollStart[0] - mouseOffset[0]), Math.max(0, scrollStart[1] - mouseOffset[1]) ]; if( scrollOffset[0] !== Math.abs(this.mcs.left) || scrollOffset[1] !== Math.abs(this.mcs.top) ){ Scrollbar.scrollToPosition(this, [scrollOffset[1], scrollOffset[0]], { scrollInertia: 0, scrollEasing: 'linear', timeout: 5 }); } // recursive re-call on next render animationFrameId = requestAnimationFrame(dragScroll); } }; let keyDownHandler = function(e){ if(e.keyCode === 32){ e.preventDefault(); toggleDragScroll(true); } }; let keyUpHandler = function(e){ if(e.keyCode === 32){ e.preventDefault(); toggleDragScroll(false); } }; let mouseMoveHandler = function(e){ if(animationFrameId){ mouseOffset[0] = e.clientX - mouseStart[0]; mouseOffset[1] = e.clientY - mouseStart[1]; } // space activated on mouse move toggleDragScroll(Key.isActive(' ')); }; let mouseDownHandler = function(e){ if(!animationFrameId && e.which === 1 && Key.isActive(' ')){ scrollStart[0] = Math.abs(this.mcs.left); scrollStart[1] = Math.abs(this.mcs.top); mouseStart[0] = e.clientX; mouseStart[1] = e.clientY; toggleDragScroll(true); animationFrameId = requestAnimationFrame(dragScroll); } }; let mouseUpHandler = function(e){ if(e.which === 1){ stopDragScroll(); } }; this.addEventListener('keydown', keyDownHandler, {capture: false}); this.addEventListener('keyup', keyUpHandler, {capture: false}); this.addEventListener('mousemove', mouseMoveHandler, {capture: false}); this.addEventListener('mousedown', mouseDownHandler, {capture: false}); this.addEventListener('mouseup', mouseUpHandler, {capture: false}); }, onScroll: function(){ // scroll complete // update scroll position for drag-frame-selection mapElement.attr('data-scroll-left', this.mcs.left); mapElement.attr('data-scroll-top', this.mcs.top); // store new map scrollOffset -> localDB Util.getLocalStore('map').setItem(`${mapId}.scrollOffset`, { x: Math.abs(this.mcs.left), y: Math.abs(this.mcs.top) }); }, onScrollStart: function(){ // hide all open xEditable fields $(this).find('.editable.editable-open').editable('hide'); // hide all system head tooltips $(this).find('.' + config.systemHeadClass + ' .fa').tooltip('hide'); }, whileScrolling: function(){ if(dragSelect){ dragSelect.update(); } } } }); // ------------------------------------------------------------------------------------------------------------ // add map overlays after scrollbar is initialized // because of its absolute position areaMap.initMapOverlays(); areaMap.initLocalOverlay(mapId); }; return { getMapInstance: getMapInstance, loadMap: loadMap, updateUserData: updateUserData, getMapDataForSync: getMapDataForSync, saveSystemCallback: saveSystemCallback, drawConnection: drawConnection, saveConnection: saveConnection }; });