Files
pathfinder/js/app/map/map.js
Mark Friedrich 8b2cac1d58 - Improved "signature type" map overlays on "connection endpoints", closed #725
- Upgraded "[localForage](https://localforage.github.io/localForage)", JS lib `v1.4.2` → `v1.7.3`
- Upgraded "[Mustache](http://mustache.github.io)", JS lib `v1.0.0` → `v3.0.1`
2019-05-11 14:02:28 +02:00

3085 lines
118 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Main map functionality
*/
define([
'jquery',
'app/init',
'app/util',
'bootbox',
'app/map/util',
'app/map/contextmenu',
'app/map/overlay',
'app/map/system',
'app/map/layout',
'app/map/magnetizing',
'app/map/scrollbar',
'dragToSelect',
'app/map/local'
], ($, Init, Util, bootbox, MapUtil, MapContextMenu, MapOverlay, System, Layout, MagnetizerWrapper) => {
'use strict';
let config = {
zIndexCounter: 110,
maxActiveConnections: 8,
mapWrapperClass: 'pf-map-wrapper', // wrapper div (scrollable)
mapClass: 'pf-map', // class for all maps
mapIdPrefix: 'pf-map-', // id prefix for all maps
systemClass: 'pf-system', // class for all systems
systemActiveClass: 'pf-system-active', // class for an active system on a map
systemSelectedClass: 'pf-system-selected', // class for selected systems on a map
systemLockedClass: 'pf-system-locked', // class for locked 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
dynamicElementWrapperId: 'pf-dialog-wrapper', // wrapper div for dynamic content (dialogs, context-menus,...)
// 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 = [];
/**
* 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,
anchor: 'Continuous'
},
target: {
filter: filterSystemHeadEvent,
isSource: true,
//isTarget:true,
//allowLoopBack: false, // loopBack connections are not allowed
cssClass: config.endpointTargetClass,
dropOptions: {
hoverClass: config.systemActiveClass,
activeClass: 'dragActive'
},
// isTarget:true,
// uniqueEndpoint: false,
anchor: 'Continuous'
},
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(MapOverlay.endpointOverlayId);
if(overlay instanceof jsPlumb.Overlays.Label){
let label = overlay.getParameter('label');
overlay.setLocation(MapUtil.getEndpointOverlaySignatureLocation(endpoint, label));
}
}
}
}
};
/**
* updates a system with current information
* @param map
* @param data
* @param currentUserIsHere boolean - if the current user is in this system
* @param options
*/
$.fn.updateSystemUserData = function(map, data, currentUserIsHere, options){
let system = $(this);
let systemId = 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('userCache');
let oldUserCount = system.data('userCount');
oldUserCount = (oldUserCount !== undefined ? oldUserCount : 0);
let userCounter = 0;
system.data('currentUser', false);
// if current user is in THIS system trigger event
if(currentUserIsHere){
system.data('currentUser', true);
}
// add user information
if(
data &&
data.user
){
let cacheArray = [];
// we need to add "view mode" option to key
// -> if view mode change detected -> key no longer valid
cacheArray.push(compactView ? 'compact' : 'default');
// loop all active pilots and build cache-key
for(let i = 0; i < data.user.length; i++){
userCounter++;
let tempUserData = data.user[i];
cacheArray.push(tempUserData.id + '_' + tempUserData.log.ship.id);
}
let cacheKey = cacheArray.join('_');
// check for if cacheKey has changed
if(cacheKey !== oldCacheKey){
// set new CacheKey
system.data('userCache', 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(systemId);
}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 = $('<div>', {
class: config.systemBodyItemClass
}).append(
$('<span>', {
text: userData.log.ship.typeName,
class: config.systemBodyRightClass
})
).append(
$('<i>', {
class: ['fas', 'fa-circle', config.systemBodyItemStatusClass, statusClass].join(' ')
})
).append(
$('<span>', {
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: systemId,
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( systemId );
}
});
}
}
}else{
// no user data found for this system
system.data('userCache', 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(systemId);
}
}
};
/**
* 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 = $('<div>', {
id: systemId,
class: config.systemClass
}).append(
$('<div>', {
class: config.systemHeadClass
}).append(
$('<span>', {
class: [config.systemSec, secClass].join(' '),
text: data.security
}),
// System name is editable
$('<span>', {
class: systemHeadClasses.join(' '),
}).attr('data-value', systemName),
// System users count
$('<span>', {
class: [config.systemHeadCounterClass, Util.config.popoverTriggerClass].join(' ')
}),
// System locked status
$('<i>', {
class: ['fas', 'fa-lock', 'fa-fw'].join(' ')
}).attr('title', 'locked'),
// System effect color
$('<i>', {
class: ['fas', 'fa-square', 'fa-fw', effectBasicClass, effectClass, Util.config.popoverTriggerClass].join(' ')
}),
// expand option
$('<i>', {
class: ['fas', 'fa-angle-down', config.systemHeadExpandClass].join(' ')
}),
// info element (new line) (optional)
System.getHeadInfoElement(data)
),
$('<div>', {
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('faction', data.faction);
system.data('planets', data.planets);
system.data('shattered', data.shattered);
system.data('statics', data.statics);
system.data('updated', parseInt(data.updated.updated));
system.data('changed', false);
system.attr('data-mapid', parseInt(mapContainer.data('id')));
// 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('.' + 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
system.getMapOverlay('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(systemData, action);
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 grid = [MapUtil.config.mapSnapToGridDimension, MapUtil.config.mapSnapToGridDimension];
let positionFinder = new Layout.Position({
container: mapElement[0],
center: [position.x, position.y],
loops: 5,
defaultGapX: 10,
defaultGapY: 10,
grid: mapElement.hasClass(MapUtil.config.mapGridClass) ? grid : false,
debug: false
});
let dimensions = positionFinder.findNonOverlappingDimensions(1, 8);
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');
let promiseStore = MapUtil.getLocaleData('map', mapId);
promiseStore.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 = [];
}
}
// save filterScopes in IndexDB
MapUtil.storeLocalData('map', 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
$(document).triggerMenuEvent('ShowMapSettings', {tab: 'edit'});
break;
case 'map_info':
// open map info dialog tab
$(document).triggerMenuEvent('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
// confirm dialog
bootbox.confirm('Is this connection really gone?', function(result){
if(result){
MapUtil.deleteConnections([connection]);
}
});
break;
case 'frigate': // set as frigate hole
case 'preserve_mass': // set "preserve mass
case 'wh_eol': // set "end of life"
mapElement.getMapOverlay('timer').startMapUpdateCounter();
connection.toggleType(action);
MapUtil.markAsChanged(connection);
break;
case 'status_fresh':
case 'status_reduced':
case 'status_critical':
let newStatus = action.split('_')[1];
mapElement.getMapOverlay('timer').startMapUpdateCounter();
MapUtil.setConnectionWHStatus(connection, 'wh_' + newStatus);
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 + '?', function(result){
if(result){
mapElement.getMapOverlay('timer').startMapUpdateCounter();
setConnectionScope(connection, newScope);
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':
mapElement.getMapOverlay('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);
// remove all connection types
connection.clearTypes();
// set new new connection type
// if scope changed -> connection type == scope
connection.setType(MapUtil.getDefaultConnectionTypeByScope(scope));
// change scope
connection.scope = scope;
}
};
/**
* connect two systems
* @param map
* @param connectionData
* @returns new connection
*/
let drawConnection = (map, connectionData) => {
let mapContainer = $(map.getContainer());
let mapId = mapContainer.data('id');
let connectionId = connectionData.id || 0;
let connection;
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 &&
targetSystem.length
){
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){
// 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);
}
}
}
}
}
}else{
if( !sourceSystem.length ){
console.warn('drawConnection(): source system (id: ' + connectionData.source + ') not found');
}
if( !targetSystem.length ){
console.warn('drawConnection(): target system (id: ' + connectionData.target + ') not found');
}
}
return connection;
};
/**
* compares the current data and new data of a connection and updates status
* @param connection
* @param newConnectionData
* @returns {*}
*/
let updateConnection = (connection, newConnectionData) => {
let connectionData = MapUtil.getDataByConnection(connection);
let map = connection._jsPlumb.instance;
let mapContainer = $(map.getContainer());
let mapId = mapContainer.data('id');
// type "process" is not included in connectionData
// -> if "process" type exists, add it types for removal
if(connection.hasType('state_process')){
connectionData.type.push('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(connectionData.scope !== newConnectionData.scope){
setConnectionScope(connection, newConnectionData.scope);
}
let addType = newConnectionData.type.diff(connectionData.type);
let removeType = connectionData.type.diff(newConnectionData.type);
// update source/target (after drag&drop) ---------------------------------------------------------------------
if(connectionData.source !== newConnectionData.source){
map.setSource(connection, MapUtil.getSystemId(mapId, newConnectionData.source));
}
if(connectionData.target !== newConnectionData.target){
map.setTarget(connection, MapUtil.getSystemId(mapId, newConnectionData.target));
}
// update connection types ------------------------------------------------------------------------------------
let checkAvailability = (arr, val) => arr.some(arrVal => arrVal === val);
for(let type of addType){
if(checkAvailability(['fresh', 'reduced', 'critical'], type)){
MapUtil.setConnectionWHStatus(connection, type);
}else if(connection.hasType(type) !== true){
// additional types e.g. eol, frig, preserve mass
connection.addType(type);
}
}
for(let type of removeType){
if(checkAvailability(['wh_eol', 'frigate', 'preserve_mass', 'state_process'], type)){
connection.removeType(type);
}
}
// 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 wrapper observer
* @param mapWrapper
* @param mapConfig
*/
let setMapWrapperObserver = (mapWrapper, 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;
let promiseStore = MapUtil.getLocaleData('map', mapConfig.config.id );
promiseStore.then((data) => {
let storeData = true;
if(
data && data.style &&
data.style.width === width &&
data.style.height === height
){
// no style changes
storeData = false;
}
if(storeData){
MapUtil.storeLocalData('map', 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(mapWrapper[0]);
}else if(requestAnimationFrame){
// ResizeObserver() not supported
let checkMapSize = (entry) => {
saveMapSize(entry);
return setTimeout(checkMapSize, 500, entry);
};
checkMapSize(mapWrapper[0]);
}
};
/**
* get a mapMapElement
* @param parentElement
* @param mapConfig
* @returns {Promise<any>}
*/
let newMapElement = (parentElement, mapConfig) => {
/**
* new map element promise
* @param resolve
* @param reject
*/
let newMapElementExecutor = (resolve, reject) => {
// get map dimension from local storage
let promiseStore = MapUtil.getLocaleData('map', mapConfig.config.id );
promiseStore.then((data) => {
let height = 0;
if(data && data.style){
height = data.style.height;
}
// create map wrapper
let mapWrapper = $('<div>', {
class: config.mapWrapperClass,
height: height
});
setMapWrapperObserver(mapWrapper, mapConfig);
let mapId = mapConfig.config.id;
// create new map container
let mapContainer = $('<div>', {
id: config.mapIdPrefix + mapId,
class: config.mapClass
}).data('id', mapId);
mapWrapper.append(mapContainer);
// append mapWrapper to parent element (at the top)
parentElement.prepend(mapWrapper);
// set main Container for current map -> the container exists now in DOM !! very important
mapConfig.map.setContainer(mapContainer);
// init custom scrollbars and add overlay
parentElement.initMapScrollbar();
// set map observer
setMapObserver(mapConfig.map);
// set shortcuts
mapWrapper.setMapShortcuts();
// show static overlay actions
let mapOverlay = mapContainer.getMapOverlay('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<any>}
*/
let updateMap = mapConfig => {
/**
* update map promise
* @param resolve
* @param reject
*/
let updateMapExecutor = (resolve, reject) => {
// 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){
let mapId = mapConfig.config.id;
let newSystems = 0;
// 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);
}
// get map data
let mapData = getMapDataForSync(mapContainer, [], true);
if(mapData !== false){
// 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);
newSystems++;
}
}
// 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 batch() is used, otherwise there are some "strange" visual bugs
// when switching maps (Endpoints are not displayed correctly)
mapConfig.map.batch(function(){
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);
}
}
// 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.detach(deleteConnection, {fireEvent: false});
}
}
}
}
});
// update local connection cache
updateConnectionsCache(mapConfig.map);
// update map "magnetization" when new systems where added
if(newSystems > 0){
MagnetizerWrapper.setElements(mapConfig.map);
}
}else{
// map is currently logged -> queue update for this map until unlock
if( mapUpdateQueue.indexOf(mapId) === -1 ){
mapUpdateQueue.push(mapId);
}
}
}
resolve({
action: 'updateMap',
data: {
mapConfig: mapConfig
}
});
};
return new Promise(updateMapExecutor).then(payload => {
let filterMapByScopesExecutor = (resolve, reject) => {
// apply current active scope filter ==================================================================
let promiseStore = MapUtil.getLocaleData('map', payload.data.mapConfig.config.id);
promiseStore.then(dataStore => {
let scopes = [];
if(dataStore && dataStore.filterScopes){
scopes = dataStore.filterScopes;
}
MapUtil.filterMapByScopes(payload.data.mapConfig.map, scopes);
resolve(payload);
});
};
return new Promise(filterMapByScopesExecutor);
});
};
/**
* 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 to be actual!
* @param mapId
* @param connectionId
* @returns {*}
*/
$.fn.getConnectionById = function(mapId, connectionId){
let connection = null;
if(
connectionCache[mapId] &&
connectionCache[mapId][connectionId]
){
connection = connectionCache[mapId][connectionId];
}
return connection;
};
/**
* 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 => {
let isValid = true;
if(
!systemData.hasOwnProperty('name') ||
systemData.name.length === 0
){
return false;
}
return isValid;
};
/**
* draw a system with its data to a map
* @param map
* @param systemData
* @param connectedSystem
*/
let drawSystem = (map, systemData, connectedSystem) => {
// check if systemData is valid
if(isValidSystem(systemData)){
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);
// 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,..
let connectionData = {
source: $(connectedSystem).data('id'),
target: newSystem.data('id'),
scope: map.Defaults.Scope,
type: [MapUtil.getDefaultConnectionTypeByScope(map.Defaults.Scope)]
};
let connection = drawConnection(map, connectionData);
// store connection
saveConnection(connection);
}
}
};
/**
* 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',
container: 'body',
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
*/
let saveConnection = connection => {
if(connection 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;
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'});
}else{
// some save errors
payload.context.map.detach(payload.context.connection, {fireEvent: false});
}
},
payload => {
// remove this connection from map
payload.context.map.detach(payload.context.connection, {fireEvent: false});
Util.handleAjaxErrorResponse(payload);
}
);
}
};
/**
* get context menu config for a map component (e.g. system, connection,..)
* @param component
* @returns {Promise<any>}
*/
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<any>}
*/
let getSystemContextMenuConfig = system => {
let executor = resolve => {
let options = MapContextMenu.defaultMenuOptionConfig();
options.id = MapContextMenu.config.systemContextMenuId;
options.selectCallback = systemActions;
let mapContainer = system.closest('.' + config.mapClass);
// hidden menu actions
if(system.data('locked') === true){
options.hidden.push('delete_system');
}
if( !mapContainer.find('.' + 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(config.systemActiveClass)){
options.disabled.push('find_route');
}
resolve(options);
};
return new Promise(executor);
};
/**
* get context menu config for map
* @param map
* @returns {Promise<any>}
*/
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
let promiseStore = MapUtil.getLocaleData('map', mapContainer.data('id'));
promiseStore.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<any>}
*/
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('frigate');
options.hidden.push('preserve_mass');
options.hidden.push('change_status');
options.hidden.push('change_scope');
options.hidden.push('separator');
}else if(scope === 'stargate'){
options.hidden.push('frigate');
options.hidden.push('preserve_mass');
options.hidden.push('change_status');
options.hidden.push('scope_stargate');
}else if(scope === 'jumpbridge'){
options.hidden.push('frigate');
options.hidden.push('preserve_mass');
options.hidden.push('change_status');
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('frigate') === true){
options.active.push('frigate');
}
if(connection.hasType('preserve_mass') === true){
options.active.push('preserve_mass');
}
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');
}
resolve(options);
};
return new Promise(executor);
};
/**
* get context menu config for endpoint
* @param endpoint
* @returns {Promise<any>}
*/
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;
// 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);
mapOverlayTimer = dragSystem.getMapOverlay('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 );
}
// stop "system click event" right after drop event is finished
dragSystem.addClass('no-click');
// 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){
// 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)
MagnetizerWrapper.executeAtEvent(map, p.e);
},
stop: function(params){
let dragSystem = $(params.el);
// start map update timer
mapOverlayTimer.startMapUpdateCounter();
setTimeout(function(){
dragSystem.removeClass('no-click');
}, Init.timer.DBL_CLICK + 50);
// 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);
// drag system is not always selected
let selectedSystems = mapContainer.getSelectedSystems().get();
selectedSystems = selectedSystems.concat(dragSystem.get());
selectedSystems = $.unique( selectedSystems );
// repaint connections (and overlays) -> just in case something fails...
revalidate(map, selectedSystems);
}
});
if(system.data('locked') === true){
map.setDraggable(system, false);
}
// init system tooltips =======================================================================================
let systemTooltipOptions = {
toggle: 'tooltip',
placement: 'right',
container: 'body',
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);
// check if system is locked for "click" events
if(!system.hasClass('no-click')){
// 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, single, double);
};
/**
* callback after system save
* @param map
* @param newSystemData
* @param sourceSystem
*/
let saveSystemCallback = (map, newSystemData, sourceSystem) => {
// draw new system to map
drawSystem(map, newSystemData, sourceSystem);
// re/arrange systems (prevent overlapping)
MagnetizerWrapper.setElements(map);
};
/**
* 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( 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( 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){
$(system).getMapOverlay('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', // anchors on each site
Container: null, // will be set as soon as container is connected to DOM
PaintStyle: {
lineWidth: 4, // width of a Connector's line. An integer.
strokeStyle: 'red', // color for a Connector
outlineColor: 'red', // color of the outline for an Endpoint or Connector. see fillStyle examples.
outlineWidth: 2 // width of the outline for an Endpoint or Connector. An integer.
},
Connector: [ 'Bezier', { curviness: 40 } ], // default connector style (this is not used!) all connections have their own style (by scope)
Endpoint: [ 'Dot', { radius: 5 } ],
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;
}
// lock the target system for "click" events
// to prevent loading system information
let sourceSystem = $('#' + sourceId);
let targetSystem = $('#' + targetId);
sourceSystem.addClass('no-click');
targetSystem.addClass('no-click');
setTimeout(() => {
sourceSystem.removeClass('no-click');
targetSystem.removeClass('no-click');
}, Init.timer.DBL_CLICK + 50);
// 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.setConnectionWHStatus(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?', function(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);
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 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);
});
});
// ========================================================================================================
// 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 <body> 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());
// 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(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
mapContainer.dragToSelect({
selectOnMove: true,
selectables: '.' + config.systemClass,
onHide: function(selectBox, 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 j = 0; j < deselectedSystems.length; j++){
map.removeFromDragSelection( deselectedSystems[j] );
}
},
onShow: function(){
$(document).trigger('pf:closeMenu', [{}]);
},
onRefresh: function(){
}
});
// 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
});
// 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)
mapContainer.hoverIntent({
over: function(e){
let staticWormholeElement = $(this);
let wormholeName = staticWormholeElement.attr('data-name');
let wormholeData = Util.getObjVal(Init, 'wormholes.' + wormholeName);
if(wormholeData){
staticWormholeElement.addWormholeInfoTooltip(wormholeData, {
trigger: 'manual',
placement: 'right',
smaller: true,
show: true
});
}
},
out: function(e){
$(this).destroyPopover();
},
selector: '.' + config.systemHeadInfoClass + ' span[class^="pf-system-sec-"]'
});
// toggle "fullSize" Endpoint overlays for system (signature information) -------------------------------------
mapContainer.hoverIntent({
over: function(e){
for(let overlayInfo of map.selectEndpoints({element: this}).getOverlay(MapOverlay.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(MapOverlay.endpointOverlayId)){
if(overlayInfo[0] instanceof jsPlumb.Overlays.Label){
overlayInfo[0].fire('toggleSize', false);
}
}
},
selector: '.' + config.systemClass
});
// catch events ===============================================================================================
// toggle global map option (e.g. "grid snap", "magnetization")
mapContainer.on('pf:menuMapOption', function(e, mapOption){
let mapElement = $(this);
// get map menu config options
let data = MapUtil.mapOptions[mapOption.option];
let promiseStore = MapUtil.getLocaleData('map', mapElement.data('id'));
promiseStore.then(function(dataStore){
let notificationText = 'disabled';
let button = $('#' + this.data.buttonId);
let dataExists = false;
if(
dataStore &&
dataStore[this.mapOption.option]
){
dataExists = true;
}
if(dataExists === mapOption.toggle){
// toggle button class
button.removeClass('active');
// toggle map class (e.g. for grid)
if(this.data.class){
this.mapElement.removeClass( MapUtil.config[this.data.class] );
}
// call optional jQuery extension on mapElement
if(this.data.onDisable){
$.fn[ this.data.onDisable ].apply(this.mapElement);
}
// show map overlay info icon
this.mapElement.getMapOverlay('info').updateOverlayIcon(this.mapOption.option, 'hide');
// delete map option
MapUtil.deleteLocalData('map', this.mapElement.data('id'), this.mapOption.option );
}else{
// toggle button class
button.addClass('active');
// toggle map class (e.g. for grid)
if(this.data.class){
this.mapElement.addClass( MapUtil.config[this.data.class] );
}
// call optional jQuery extension on mapElement
if(this.data.onEnable){
$.fn[ this.data.onEnable ].apply(this.mapElement);
}
// hide map overlay info icon
this.mapElement.getMapOverlay('info').updateOverlayIcon(this.mapOption.option, 'show');
// store map option
MapUtil.storeLocalData('map', this.mapElement.data('id'), this.mapOption.option, 1 );
notificationText = 'enabled';
}
if(mapOption.toggle){
Util.showNotify({title: this.data.description, text: notificationText, type: 'info'});
}
}.bind({
mapOption: mapOption,
data: data,
mapElement: mapElement
}));
});
// delete system event
// triggered from "map info" dialog scope
mapContainer.on('pf:deleteSystems', function(e, data){
System.deleteSystems(map, data.systems, data.callback);
});
// triggered from "header" link (if user is active in one of the systems)
mapContainer.on('pf:menuSelectSystem', function(e, data){
let mapElement = $(this);
let systemId = MapUtil.getSystemId(mapElement.data('id'), data.systemId);
let system = mapElement.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 mapWrapper = mapElement.closest('.' + config.mapWrapperClass);
mapWrapper.scrollToSystem(MapUtil.getSystemPosition(system));
// select system
MapUtil.showSystemInfo(map, system);
}
}
});
// 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 mapElement = $(this);
let mapOverlay = mapElement.getMapOverlay('local');
if(userData && userData.config && userData.config.id){
let currentMapData = Util.getCurrentMapData(userData.config.id);
let currentCharacterLog = Util.getCurrentCharacterLog();
let clearLocal = true;
if(
currentMapData &&
currentCharacterLog &&
currentCharacterLog.system
){
let currentSystemData = currentMapData.data.systems.filter(function(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<any>}
*/
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 characterLogExists = false;
let currentCharacterLog = Util.getCurrentCharacterLog();
// 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, // inactive users (no location)
currentLocation: {
id: 0, // systemId for current active user
name: false // systemName for current active user
}
};
if(
currentCharacterLog &&
currentCharacterLog.system
){
characterLogExists = true;
headerUpdateData.currentLocation.name = currentCharacterLog.system.name;
}
// check if current user was found on the map
let currentUserOnMap = false;
// get all systems
let systems = mapElement.find('.' + config.systemClass);
for(let system of systems){
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(
characterLogExists &&
currentCharacterLog.system.id === systemId
){
if( !currentUserOnMap ){
currentUserIsHere = true;
currentUserOnMap = true;
// set current location data for header update
headerUpdateData.currentLocation.id = system.data('id');
headerUpdateData.currentLocation.name = currentCharacterLog.system.name;
}
}
system.updateSystemUserData(map, 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 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(!mapContainer.getMapOverlayInterval()){
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
*/
$.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 systemData = {
id: parseInt(system.data('id')),
updated: {
updated: parseInt(system.data('updated'))
}
};
if(!minimal){
systemData = Object.assign(systemData, {
systemId: parseInt(system.data('systemId')),
name: system.data('name'),
alias: system.getSystemInfo(['alias']),
effect: system.data('effect'),
type: {
id: system.data('typeId')
},
security: system.data('security'),
trueSec: system.data('trueSec'),
region: {
id: system.data('regionId'),
name: system.data('region')
},
constellation: {
id: system.data('constellationId'),
name: system.data('constellation')
},
status: {
id: system.data('statusId')
},
locked: system.data('locked') ? 1 : 0,
rallyUpdated: system.data('rallyUpdated') || 0,
rallyPoke: system.data('rallyPoke') ? 1 : 0,
currentUser: system.data('currentUser'), // if user is currently in this system
faction: system.data('faction'),
planets: system.data('planets'),
shattered: system.data('shattered') ? 1 : 0,
statics: system.data('statics'),
userCount: (system.data('userCount') ? parseInt(system.data('userCount')) : 0),
position: MapUtil.getSystemPosition(system)
});
}
return systemData;
};
/**
* init map options
* @param mapConfig
* @param options
* @returns {Promise<any>}
*/
let initMapOptions = (mapConfig, options) => {
let initMapOptionsExecutor = (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.scrollToDefaultPosition(mapElement))
.then(payload => {
Util.showNotify({title: 'Map initialized', text: mapConfig.config.name + ' - loaded', type: 'success'});
})
.then(() => resolve(payload));
}else{
// nothing to do here...
resolve(payload);
}
};
return new Promise(initMapOptionsExecutor);
};
/**
* load OR updates system map
* @param tabContentElement parent element where the map will be loaded
* @param mapConfig
* @param options
* @returns {Promise<any>}
*/
let loadMap = (tabContentElement, mapConfig, options) => {
/**
* load map promise
* @param resolve
* @param reject
*/
let loadMapExecutor = (resolve, reject) => {
// init jsPlumb
jsPlumb.ready(function(){
// get new map instance or load existing
mapConfig.map = getMapInstance(mapConfig.config.id);
if(mapConfig.map.getContainer() === undefined){
// map not loaded -> create & update
newMapElement(tabContentElement, 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));
};
/**
* init scrollbar for Map element
*/
$.fn.initMapScrollbar = function(){
// get Map Scrollbar
let mapTabContentElement = $(this);
let mapWrapperElement = mapTabContentElement.find('.' + config.mapWrapperClass);
let mapElement = mapTabContentElement.find('.' + config.mapClass);
let mapId = mapElement.data('id');
mapWrapperElement.initCustomScrollbar({
callbacks: {
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
MapUtil.storeLocalData('map', 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('hide');
// hide all system head tooltips
$(this).find('.' + config.systemHeadClass + ' .fa').tooltip('hide');
}
}
});
// ------------------------------------------------------------------------------------------------------------
// add map overlays after scrollbar is initialized
// because of its absolute position
mapWrapperElement.initMapOverlays();
mapWrapperElement.initLocalOverlay(mapId);
};
return {
getMapInstance: getMapInstance,
loadMap: loadMap,
updateUserData: updateUserData,
getMapDataForSync: getMapDataForSync,
saveSystemCallback: saveSystemCallback
};
});