', {
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
};
});