diff --git a/.gitignore b/.gitignore index dd54d994..f5b99ae3 100644 --- a/.gitignore +++ b/.gitignore @@ -50,10 +50,10 @@ Temporary Items .sass-cache .usage *.gz -*.lock +composer-dev.lock +package-lock.json /conf/ /node_modules/ /public/js/vX.X.X/ /vendor/ /history/ -/package-lock.json diff --git a/README.md b/README.md index 082c9cfb..8cc71042 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,8 @@ Issues should be reported in the [Issue](https://github.com/exodus4d/pathfinder/ *** -### Thanks! +### Contributing + +[![](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/images/0)](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/links/0)[![](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/images/1)](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/links/1)[![](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/images/2)](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/links/2)[![](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/images/3)](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/links/3)[![](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/images/4)](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/links/4)[![](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/images/5)](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/links/5)[![](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/images/6)](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/links/6)[![](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/images/7)](https://sourcerer.io/fame/exodus4d/exodus4d/pathfinder/links/7) + -It took me month of time in development until this project got into the first *BETA*. If you like it, please help to improve it. -(report bugs, find security issues,...) diff --git a/app/lib/cron.php b/app/lib/cron.php index bae09128..563783c2 100644 --- a/app/lib/cron.php +++ b/app/lib/cron.php @@ -119,9 +119,9 @@ class Cron extends \Prefab { if (!preg_match($this->windows?'/^[A-Z]:\\\\/i':'/^\//',$dir)) $dir=getcwd().'/'.$dir; if ($this->windows) { - pclose(popen(sprintf('start "cron" "%s" "%s\\%s" "/cron/%s"',$this->binary,$dir,$file,$job),'r')); + pclose(popen(sprintf('start /b "cron" "%s" "%s\\%s" "/cron/%s"',$this->binary,$dir,$file,$job),'r')); } else { - exec(sprintf('cd "%s" & %s %s /cron/%s >/dev/null 2>/dev/null &',$dir,$this->binary,$file,$job)); + exec(sprintf('cd "%s" && %s %s /cron/%s >/dev/null 2>/dev/null &',$dir,$this->binary,$file,$job)); } return FALSE; } @@ -256,4 +256,4 @@ class Cron extends \Prefab { $f3->route(array('GET /cron','GET /cron/@job'),array($this,'route')); } -} +} \ No newline at end of file diff --git a/app/main/controller/accesscontroller.php b/app/main/controller/accesscontroller.php index 281cb325..26692c7e 100644 --- a/app/main/controller/accesscontroller.php +++ b/app/main/controller/accesscontroller.php @@ -74,15 +74,25 @@ class AccessController extends Controller { return $loginStatus; } + /** + * broadcast MapModel to clients + * @see broadcastMapData() + * @param Pathfinder\MapModel $map + */ + protected function broadcastMap(Pathfinder\MapModel $map) : void { + $this->broadcastMapData($this->getFormattedMapData($map)); + } + + /** * broadcast map data to clients * -> send over TCP Socket - * @param Pathfinder\MapModel $map - * @throws \Exception + * @param array|null $mapData */ - protected function broadcastMapData(Pathfinder\MapModel $map) : void { - $mapData = $this->getFormattedMapData($map); - $this->getF3()->webSocket()->write('mapUpdate', $mapData); + protected function broadcastMapData(?array $mapData) : void { + if(!empty($mapData)){ + $this->getF3()->webSocket()->write('mapUpdate', $mapData); + } } /** @@ -91,16 +101,27 @@ class AccessController extends Controller { * @return array * @throws \Exception */ - protected function getFormattedMapData(Pathfinder\MapModel $map) : array { - $mapData = $map->getData(); - return [ - 'config' => $mapData->mapData, - 'data' => [ - 'systems' => $mapData->systems, - 'connections' => $mapData->connections, - ] - ]; + /** + * @param Pathfinder\MapModel $map + * @return array|null + */ + protected function getFormattedMapData(Pathfinder\MapModel $map) : ?array { + $data = null; + try{ + $mapData = $map->getData(); + $data = [ + 'config' => $mapData->mapData, + 'data' => [ + 'systems' => $mapData->systems, + 'connections' => $mapData->connections, + ] + ]; + }catch(\Exception $e){ + + } + + return $data; } } \ No newline at end of file diff --git a/app/main/controller/api/map.php b/app/main/controller/api/map.php index 84e56c7a..19f6fd04 100644 --- a/app/main/controller/api/map.php +++ b/app/main/controller/api/map.php @@ -25,7 +25,6 @@ class Map extends Controller\AccessController { // cache keys const CACHE_KEY_INIT = 'CACHED_INIT'; - const CACHE_KEY_MAP_DATA = 'CACHED.MAP_DATA.%s'; const CACHE_KEY_USER_DATA = 'CACHED.USER_DATA.%s'; const CACHE_KEY_HISTORY = 'CACHED_MAP_HISTORY_%s'; @@ -652,6 +651,7 @@ class Map extends Controller\AccessController { protected function broadcastMapAccess(Pathfinder\MapModel $map){ $mapAccess = [ 'id' => $map->_id, + 'name' => $map->name, 'characterIds' => array_map(function ($data){ return $data->id; }, $map->getCharactersData()) @@ -660,7 +660,7 @@ class Map extends Controller\AccessController { $this->getF3()->webSocket()->write('mapAccess', $mapAccess); // map has (probably) active connections that should receive map Data - $this->broadcastMapData($map); + $this->broadcastMap($map); } /** @@ -689,18 +689,22 @@ class Map extends Controller\AccessController { unset($characterData->corporation->rights); } + // access token + $token = bin2hex(random_bytes(16)); + $return->data = [ - 'id' => $activeCharacter->_id, - 'token' => bin2hex(random_bytes(16)), // token for character access + 'id' => $activeCharacter->_id, + 'token' => $token, // character access 'characterData' => $characterData, - 'mapData' => [] + 'mapData' => [] ]; if($maps){ foreach($maps as $map){ $return->data['mapData'][] = [ - 'id' => $map->_id, - 'token' => bin2hex(random_bytes(16)) // token for map access + 'id' => $map->_id, + 'token' => $token, // map access + 'name' => $map->name ]; } } @@ -721,41 +725,33 @@ class Map extends Controller\AccessController { } /** - * update map data - * -> function is called continuously (trigger) by any active client - * @param \Base $f3 - * @throws Exception + * update maps with $mapsData where $character has access to + * @param Pathfinder\CharacterModel $character + * @param array $mapsData + * @return \stdClass */ - public function updateData(\Base $f3){ - $postData = (array)$f3->get('POST'); - $mapData = (array)$postData['mapData']; - $userDataRequired = (bool)$postData['getUserData']; - + protected function updateMapsData(Pathfinder\CharacterModel $character, array $mapsData) : \stdClass { $return = (object) []; $return->error = []; + $return->mapData = []; - $activeCharacter = $this->getCharacter(); + $mapIdsChanged = []; + $maps = $character->getMaps(); - // get current map data - $maps = $activeCharacter->getMaps(); - - // if there is any system/connection change data submitted -> save new data - if( !empty($maps) && !empty($mapData) ){ - - // loop all submitted map data that should be saved + if(!empty($mapsData) && !empty($maps)){ + // loop all $mapsData that should be saved // -> currently there will only be ONE map data change submitted -> single loop - foreach($mapData as $data){ - + foreach($mapsData as $data){ $systems = []; $connections = []; // check whether system data and/or connection data is send // empty arrays are not included in ajax requests - if( isset($data['data']['systems']) ){ + if(isset($data['data']['systems'])){ $systems = (array)$data['data']['systems']; } - if( isset($data['data']['connections']) ){ + if(isset($data['data']['connections'])){ $connections = (array)$data['data']['connections']; } @@ -768,15 +764,15 @@ class Map extends Controller\AccessController { // loop current user maps and check for changes foreach($maps as $map){ - $mapChanged = false; - // update system data ------------------------------------------------------------------------- foreach($systems as $i => $systemData){ // check if current system belongs to the current map if($system = $map->getSystemById((int)$systemData['id'])){ $system->copyfrom($systemData, ['alias', 'status', 'position', 'locked', 'rallyUpdated', 'rallyPoke']); - if($system->save($activeCharacter)){ - $mapChanged = true; + if($system->save($character)){ + if(!in_array($map->_id, $mapIdsChanged)){ + $mapIdsChanged[] = $map->_id; + } // one system belongs to ONE map -> speed up for multiple maps unset($systemData[$i]); }else{ @@ -790,8 +786,10 @@ class Map extends Controller\AccessController { // check if the current connection belongs to the current map if($connection = $map->getConnectionById((int)$connectionData['id'])){ $connection->copyfrom($connectionData, ['scope', 'type', 'endpoints']); - if($connection->save($activeCharacter)){ - $mapChanged = true; + if($connection->save($character)){ + if(!in_array($map->_id, $mapIdsChanged)){ + $mapIdsChanged[] = $map->_id; + } // one connection belongs to ONE map -> speed up for multiple maps unset($connectionData[$i]); }else{ @@ -799,17 +797,39 @@ class Map extends Controller\AccessController { } } } - - if($mapChanged){ - $this->broadcastMapData($map); - } } } } } - // format map Data for return - $return->mapData = $this->getFormattedMapsData($maps); + foreach($maps as $map){ + // format map Data for return/broadcast + if($mapData = $this->getFormattedMapData($map)){ + if(in_array($map->_id, $mapIdsChanged)){ + $this->broadcastMapData($mapData); + } + + $return->mapData[] = $mapData; + } + } + + return $return; + } + + /** + * update map data + * -> function is called continuously (trigger) by any active client + * @param \Base $f3 + * @throws Exception + */ + public function updateData(\Base $f3){ + $postData = (array)$f3->get('POST'); + $mapsData = (array)$postData['mapData']; + $userDataRequired = (bool)$postData['getUserData']; + + $activeCharacter = $this->getCharacter(); + + $return = $this->updateMapsData($activeCharacter, $mapsData); // if userData is requested -> add it as well // -> Only first trigger call should request this data! @@ -821,18 +841,22 @@ class Map extends Controller\AccessController { } /** - * get formatted map data - * @param Pathfinder\MapModel[] $mapModels - * @return array + * onUnload map sync + * @see https://developer.mozilla.org/docs/Web/API/Navigator/sendBeacon + * @param \Base $f3 * @throws Exception */ - protected function getFormattedMapsData(array $mapModels) : array { - $mapData = []; - foreach($mapModels as $mapModel){ - $mapData[] = $this->getFormattedMapData($mapModel); - } + public function updateUnloadData(\Base $f3){ + $postData = (array)$f3->get('POST'); - return $mapData; + if(!empty($mapsData = (string)$postData['mapData'])){ + $mapsData = (array)json_decode($mapsData, true); + if(($jsonError = json_last_error()) === JSON_ERROR_NONE){ + $activeCharacter = $this->getCharacter(); + + $this->updateMapsData($activeCharacter, $mapsData); + } + } } /** @@ -862,7 +886,7 @@ class Map extends Controller\AccessController { if( !is_null($map = $activeCharacter->getMap($mapId)) ){ // check character log (current system) and manipulate map (e.g. add new system) if($mapTracking){ - $map = $this->updateMapData($activeCharacter, $map); + $map = $this->updateMapByCharacter($map, $activeCharacter); } // mapUserData ---------------------------------------------------------------------------------------- @@ -887,7 +911,7 @@ class Map extends Controller\AccessController { // data for currently selected system $return->system = $system->getData(); $return->system->signatures = $system->getSignaturesData(); - $return->system->sigHistory = $system ->getSignaturesHistoryData(); + $return->system->sigHistory = $system->getSignaturesHistory(); $return->system->structures = $system->getStructuresData(); } @@ -904,78 +928,73 @@ class Map extends Controller\AccessController { echo json_encode($return); } - /** - * add new map connection based on current $character location - * @param Pathfinder\CharacterModel $character + * update map connections/systems based on $character´s location logs * @param Pathfinder\MapModel $map + * @param Pathfinder\CharacterModel $character * @return Pathfinder\MapModel * @throws Exception */ - protected function updateMapData(Pathfinder\CharacterModel $character, Pathfinder\MapModel $map){ - + protected function updateMapByCharacter(Pathfinder\MapModel $map, Pathfinder\CharacterModel $character) : Pathfinder\MapModel { // map changed. update cache (system/connection) changed $mapDataChanged = false; if( ( $mapScope = $map->getScope() ) && ( $mapScope->name != 'none' ) && // tracking is disabled for map - ( $log = $character->getLog() ) + ( $targetLog = $character->getLog() ) ){ // character is currently in a system + $targetSystemId = (int)$targetLog->systemId; - $sameSystem = false; - $sourceExists = true; - $targetExists = true; - - // system coordinates - $systemOffsetX = 130; - $systemOffsetY = 0; - $systemPosX = 0; - $systemPosY = 30; - - $sessionCharacter = $this->getSessionCharacterData(); - $sourceSystemId = (int)$sessionCharacter['PREV_SYSTEM_ID']; - $targetSystemId = (int)$log->systemId; + // get 'character log' from source system. If not log found -> assume $sourceLog == $targetLog + $sourceLog = $character->getLogPrevSystem($targetSystemId) ? : $targetLog; + $sourceSystemId = (int)$sourceLog->systemId; if($sourceSystemId){ $sourceSystem = null; $targetSystem = null; - // check if source and target systems are equal - // -> NO target system available - if($sourceSystemId === $targetSystemId){ - // check if previous (solo) system is already on the map - $sourceSystem = $map->getSystemByCCPId($sourceSystemId, [AbstractModel::getFilter('active', true)]); - $sameSystem = true; - }else{ - // check if previous (source) system is already on the map - $sourceSystem = $map->getSystemByCCPId($sourceSystemId, [AbstractModel::getFilter('active', true)]); + $sourceExists = false; + $targetExists = false; - // -> check if system is already on this map - $targetSystem = $map->getSystemByCCPId($targetSystemId, [AbstractModel::getFilter('active', true)]); - } + $sameSystem = false; + + // system coordinates for system tha might be added next + $systemOffsetX = 130; + $systemOffsetY = 0; + $systemPosX = 0; + $systemPosY = 30; + + // check if previous (solo) system is already on the map ---------------------------------------------- + $sourceSystem = $map->getSystemByCCPId($sourceSystemId, [AbstractModel::getFilter('active', true)]); // if systems don´t already exists on map -> get "blank" system // -> required for system type check (e.g. wormhole, k-space) - if( - !$sourceSystem && - $sourceSystemId - ){ - $sourceExists = false; - $sourceSystem = $map->getNewSystem($sourceSystemId); - }else{ + if($sourceSystem){ + $sourceExists = true; + // system exists -> add target to the "right" $systemPosX = $sourceSystem->posX + $systemOffsetX; $systemPosY = $sourceSystem->posY + $systemOffsetY; + }else{ + $sourceSystem = $map->getNewSystem($sourceSystemId); } - if( - !$sameSystem && - !$targetSystem - ){ - $targetExists = false; - $targetSystem = $map->getNewSystem($targetSystemId); + // check if source and target systems are equal ------------------------------------------------------- + if($sourceSystemId === $targetSystemId){ + $sameSystem = true; + $targetExists = $sourceExists; + $targetSystem = $sourceSystem; + }elseif($targetSystemId){ + // check if target system is already on this map + $targetSystem = $map->getSystemByCCPId($targetSystemId, [AbstractModel::getFilter('active', true)]); + + if($targetSystem){ + $targetExists = true; + }else{ + $targetSystem = $map->getNewSystem($targetSystemId); + } } // make sure we have system objects to work with @@ -1077,6 +1096,7 @@ class Map extends Controller\AccessController { } if( + !$sameSystem && $sourceExists && $targetExists && $sourceSystem && @@ -1091,17 +1111,17 @@ class Map extends Controller\AccessController { ){ // .. do not add connection if character got "podded" ------------------------------------- if( - $log->shipTypeId == 670 && + $targetLog->shipTypeId == 670 && $character->cloneLocationId ){ // .. current character location must be clone location if( ( 'station' == $character->cloneLocationType && - $character->cloneLocationId == $log->stationId + $character->cloneLocationId == $targetLog->stationId ) || ( 'structure' == $character->cloneLocationType && - $character->cloneLocationId == $log->structureId + $character->cloneLocationId == $targetLog->structureId ) ){ // .. now we need to check jump distance between systems @@ -1132,7 +1152,7 @@ class Map extends Controller\AccessController { $connection && $connection->isWormhole() ){ - $connection->logMass($log); + $connection->logMass($targetLog); } } } @@ -1140,7 +1160,7 @@ class Map extends Controller\AccessController { } if($mapDataChanged){ - $this->broadcastMapData($map); + $this->broadcastMap($map); } return $map; @@ -1171,7 +1191,7 @@ class Map extends Controller\AccessController { // get specific connections by id $connectionIds = null; if(is_array($postData['connectionIds'])){ - $connectionIds = $postData['connectionIds']; + $connectionIds = array_map('intval', $postData['connectionIds']); } $connections = $map->getConnections($connectionIds, 'wh'); diff --git a/app/main/controller/api/rest/connection.php b/app/main/controller/api/rest/connection.php index 5247da4d..9c24bfb3 100644 --- a/app/main/controller/api/rest/connection.php +++ b/app/main/controller/api/rest/connection.php @@ -56,7 +56,7 @@ class Connection extends AbstractRestController { $connectionData = $connection->getData(); // broadcast map changes - $this->broadcastMapData($connection->mapId); + $this->broadcastMap($connection->mapId); } } } @@ -86,16 +86,16 @@ class Connection extends AbstractRestController { if($map->hasAccess($activeCharacter)){ foreach($connectionIds as $connectionId){ if($connection = $map->getConnectionById($connectionId)){ - $connection->delete( $activeCharacter ); - + if($connection->delete($activeCharacter)){ + $deletedConnectionIds[] = $connectionId; + } $connection->reset(); - $deletedConnectionIds[] = $connectionId; } } // broadcast map changes if(count($deletedConnectionIds)){ - $this->broadcastMapData($map); + $this->broadcastMap($map); } } } diff --git a/app/main/controller/api/rest/signature.php b/app/main/controller/api/rest/signature.php index 00edec02..08c0bce2 100644 --- a/app/main/controller/api/rest/signature.php +++ b/app/main/controller/api/rest/signature.php @@ -82,8 +82,9 @@ class Signature extends AbstractRestController { $signatures = $system->getSignatures(); foreach($signatures as $signature){ if(!in_array($signature->_id, $updatedSignatureIds)){ - $signature->delete(); - $updateSignaturesHistory = true; + if($signature->delete()){ + $updateSignaturesHistory = true; + } } } } diff --git a/app/main/controller/api/rest/signaturehistory.php b/app/main/controller/api/rest/signaturehistory.php index 3c78b91e..f431687b 100644 --- a/app/main/controller/api/rest/signaturehistory.php +++ b/app/main/controller/api/rest/signaturehistory.php @@ -33,7 +33,7 @@ class SignatureHistory extends AbstractRestController { $system->getById($systemId); if($system->hasAccess($activeCharacter)){ - $historyDataAll = $system->getSignaturesHistoryData(); + $historyDataAll = $system->getSignaturesHistory(); foreach($historyDataAll as $historyEntry){ $label = [ $historyEntry['character']->name, @@ -74,7 +74,7 @@ class SignatureHistory extends AbstractRestController { $system = Pathfinder\AbstractPathfinderModel::getNew('SystemModel'); $system->getById($systemId, 0); if($system->hasAccess($activeCharacter)){ - if($historyEntry = $system->getSignatureHistoryData($stamp)){ + if($historyEntry = $system->getSignatureHistoryEntry($stamp)){ $updateSignaturesHistory = false; // history entry found for $stamp -> format signatures data @@ -108,8 +108,9 @@ class SignatureHistory extends AbstractRestController { $signatures = $system->getSignatures(); foreach($signatures as $signature){ if(!in_array($signature->_id, $updatedSignatureIds)){ - $signature->delete(); - $updateSignaturesHistory = true; + if($signature->delete()){ + $updateSignaturesHistory = true; + } } } diff --git a/app/main/controller/api/rest/system.php b/app/main/controller/api/rest/system.php index 94b5fe3e..48cc16e3 100644 --- a/app/main/controller/api/rest/system.php +++ b/app/main/controller/api/rest/system.php @@ -36,7 +36,7 @@ class System extends AbstractRestController { ){ $systemData = $system->getData(); $systemData->signatures = $system->getSignaturesData(); - $systemData->sigHistory = $system->getSignaturesHistoryData(); + $systemData->sigHistory = $system->getSignaturesHistory(); $systemData->structures = $system->getStructuresData(); } } @@ -144,7 +144,7 @@ class System extends AbstractRestController { } // broadcast map changes if(count($deletedSystemIds)){ - $this->broadcastMapData($map); + $this->broadcastMap($map); } } } @@ -187,13 +187,13 @@ class System extends AbstractRestController { $newSystem->clearCacheData(); // broadcast map changes - $this->broadcastMapData($newSystem->mapId); + $this->broadcastMap($newSystem->mapId); return $newSystem; } /** - * checks whether a system should be "deleted" or set "inactive" (keep some data) + * checks whether a system should be "deleted" or set "inactive" (keep persistent data) * @param Pathfinder\MapModel $map * @param Pathfinder\SystemModel $system * @return bool @@ -201,7 +201,7 @@ class System extends AbstractRestController { private function checkDeleteMode(Pathfinder\MapModel $map, Pathfinder\SystemModel $system) : bool { $delete = true; - if( !empty($system->description) ){ + if(!empty($system->description)){ // never delete systems with custom description set! $delete = false; }elseif( @@ -212,6 +212,13 @@ class System extends AbstractRestController { // map setting "persistentAliases" is active (default) AND // alias is set and != name $delete = false; + }elseif( + $map->persistentSignatures && + !empty($system->getSignatures()) + ){ + // map setting "persistentSignatures" is active (default) AND + // signatures exist + $delete = false; } return $delete; diff --git a/app/main/controller/api/route.php b/app/main/controller/api/route.php index 423babb0..2ac25c50 100644 --- a/app/main/controller/api/route.php +++ b/app/main/controller/api/route.php @@ -154,13 +154,13 @@ class Route extends Controller\AccessController { $includeTypes[] = 'wh_critical'; } - if( $filterData['wormholesFrigate'] !== true ){ - $excludeTypes[] = 'frigate'; - } - if( $filterData['wormholesEOL'] === false ){ $includeEOL = false; } + + if(!empty($filterData['excludeTypes'])){ + $excludeTypes = $filterData['excludeTypes']; + } } if( $filterData['endpointsBubble'] !== true ){ @@ -300,7 +300,7 @@ class Route extends Controller\AccessController { private function filterJumpData($filterData = [], $keepSystems = []){ if($filterData['flag'] == 'secure'){ // remove all systems (TrueSec < 0.5) from search arrays - $this->jumpArray = array_filter($this->jumpArray, function($systemId) use($keepSystems) { + $this->jumpArray = array_filter($this->jumpArray, function($systemId) use ($keepSystems) { $systemNameData = $this->nameArray[$systemId]; $systemSec = $systemNameData[3]; @@ -733,8 +733,9 @@ class Route extends Controller\AccessController { 'wormholes' => (bool) $routeData['wormholes'], 'wormholesReduced' => (bool) $routeData['wormholesReduced'], 'wormholesCritical' => (bool) $routeData['wormholesCritical'], - 'wormholesFrigate' => (bool) $routeData['wormholesFrigate'], 'wormholesEOL' => (bool) $routeData['wormholesEOL'], + 'wormholesSizeMin' => (string) $routeData['wormholesSizeMin'], + 'excludeTypes' => (array) $routeData['excludeTypes'], 'endpointsBubble' => (bool) $routeData['endpointsBubble'], 'flag' => $routeData['flag'] ]; diff --git a/app/main/controller/api/user.php b/app/main/controller/api/user.php index d036b876..03ae0c3a 100644 --- a/app/main/controller/api/user.php +++ b/app/main/controller/api/user.php @@ -189,20 +189,10 @@ class User extends Controller\Controller{ echo json_encode($return); } - /** - * delete the character log entry for the current active (main) character - * @param \Base $f3 - * @throws Exception - */ - public function deleteLog(\Base $f3){ - if($activeCharacter = $this->getCharacter()){ - $activeCharacter->logout(false, true, false); - } - } - /** * log the current user out + clear character system log data * @param \Base $f3 + * @throws Exception */ public function logout(\Base $f3){ $this->logoutCharacter($f3, false, true, true, true); diff --git a/app/main/controller/ccp/sso.php b/app/main/controller/ccp/sso.php index 75201286..27e9cf9f 100644 --- a/app/main/controller/ccp/sso.php +++ b/app/main/controller/ccp/sso.php @@ -251,7 +251,7 @@ class Sso extends Api\User{ $this->setLoginCookie($characterModel); // -> pass current character data to target page - $f3->set(Api\User::SESSION_KEY_TEMP_CHARACTER_DATA, $characterModel->_id); + $this->setTempCharacterData($characterModel->_id); // route to "map" if($rootAlias == 'admin'){ @@ -401,8 +401,7 @@ class Sso extends Api\User{ if( !empty($authCodeRequestData['expiresIn']) ){ // expire time for accessToken try{ - $timezone = $this->getF3()->get('getTimeZone')(); - $accessTokenExpires = new \DateTime('now', $timezone); + $accessTokenExpires = $this->getF3()->get('getDateTime')(); $accessTokenExpires->add(new \DateInterval('PT' . (int)$authCodeRequestData['expiresIn'] . 'S')); $accessData->esiAccessTokenExpires = $accessTokenExpires->format('Y-m-d H:i:s'); diff --git a/app/main/controller/controller.php b/app/main/controller/controller.php index 51794b65..ee020a51 100644 --- a/app/main/controller/controller.php +++ b/app/main/controller/controller.php @@ -162,6 +162,7 @@ class Controller { 'style' => $f3->get('BASE') . '/public/css/' . Config::getPathfinderData('version'), 'script' => $f3->get('BASE') . '/public/js/' . Config::getPathfinderData('version'), 'font' => $f3->get('BASE') . '/public/fonts', + 'document' => $f3->get('BASE') . '/public/templates', 'image' => $f3->get('BASE') . '/public/img' ]); @@ -387,12 +388,12 @@ class Controller { public function getSessionCharacterData() : array { $data = []; if($user = $this->getUser()){ - $header = self::getRequestHeaders(); - $requestedCharacterId = (int)$header['Pf-Character']; + $header = self::getRequestHeaders(); + $requestedCharacterId = (int)$header['Pf-Character']; if( !$this->getF3()->get('AJAX') ){ $requestedCharacterId = (int)$_COOKIE['old_char_id']; if(!$requestedCharacterId){ - $tempCharacterData = (array)$this->getF3()->get(Api\User::SESSION_KEY_TEMP_CHARACTER_DATA); + $tempCharacterData = (array)$this->getF3()->get(Api\User::SESSION_KEY_TEMP_CHARACTER_DATA); if((int)$tempCharacterData['ID'] > 0){ $requestedCharacterId = (int)$tempCharacterData['ID']; } @@ -763,17 +764,27 @@ class Controller { $return->error[] = $error; echo json_encode($return); }else{ + // non AJAX (e.g. GET/POST) + // recursively clear existing output buffers + while(ob_get_level()){ + ob_end_clean(); + } + $f3->set('tplPageTitle', 'ERROR - ' . $error->code); // set error data for template rendering $error->redirectUrl = $this->getRouteUrl(); $f3->set('errorData', $error); + // 4xx/5xx error -> set error page template if( preg_match('/^4[0-9]{2}$/', $error->code) ){ - // 4xx error -> render error page $f3->set('tplPageContent', Config::getPathfinderData('STATUS.4XX') ); }elseif( preg_match('/^5[0-9]{2}$/', $error->code) ){ $f3->set('tplPageContent', Config::getPathfinderData('STATUS.5XX')); } + + // stop script - die(); after this fkt is done + // -> unload() fkt is still called + $f3->set('HALT', true); } } @@ -859,7 +870,8 @@ class Controller { */ static function getRequestHeaders() : array { $headers = []; - + $headerPrefix = 'http_'; + $prefixLength = mb_strlen($headerPrefix); $serverData = self::getServerData(); if( @@ -874,8 +886,9 @@ class Controller { // Therefore we can´t use this for all servers // https://github.com/exodus4d/pathfinder/issues/58 foreach($_SERVER as $name => $value){ - if(substr($name, 0, 5) == 'HTTP_'){ - $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + $name = mb_strtolower($name); + if(mb_substr($name, 0, $prefixLength) == $headerPrefix){ + $headers[mb_convert_case(str_replace('_', '-', mb_substr($name, $prefixLength)), MB_CASE_TITLE)] = $value; } } } diff --git a/app/main/controller/mapcontroller.php b/app/main/controller/mapcontroller.php index c898be0d..5e0e2379 100644 --- a/app/main/controller/mapcontroller.php +++ b/app/main/controller/mapcontroller.php @@ -9,6 +9,7 @@ namespace Controller; use lib\Config; +use lib\Resource; class MapController extends AccessController { @@ -18,6 +19,9 @@ class MapController extends AccessController { * @throws \Exception */ public function init(\Base $f3) { + $resource = Resource::instance(); + $resource->register('script', 'app/mappage'); + $character = $this->getCharacter(); // characterId diff --git a/app/main/controller/setup.php b/app/main/controller/setup.php index 1b15ce4a..a38eeba4 100644 --- a/app/main/controller/setup.php +++ b/app/main/controller/setup.php @@ -24,31 +24,31 @@ class Setup extends Controller { * @var array */ protected $environmentVars = [ - 'ENVIRONMENT_CONFIG', - 'BASE', - 'URL', - 'DEBUG', - 'DB_PF_DNS', - 'DB_PF_NAME', - 'DB_PF_USER', - 'DB_PF_PASS', - 'DB_UNIVERSE_DNS', - 'DB_UNIVERSE_NAME', - 'DB_UNIVERSE_USER', - 'DB_UNIVERSE_PASS', - 'CCP_SSO_URL', - 'CCP_SSO_CLIENT_ID', - 'CCP_SSO_SECRET_KEY', - 'CCP_SSO_DOWNTIME', - 'CCP_ESI_URL', - 'CCP_ESI_DATASOURCE', - 'SMTP_HOST', - 'SMTP_PORT', - 'SMTP_SCHEME', - 'SMTP_USER', - 'SMTP_PASS', - 'SMTP_FROM', - 'SMTP_ERROR' + 'ENVIRONMENT_CONFIG' => [], + 'BASE' => ['missingOk' => true], + 'URL' => [], + 'DEBUG' => [], + 'DB_PF_DNS' => [], + 'DB_PF_NAME' => [], + 'DB_PF_USER' => [], + 'DB_PF_PASS' => [], + 'DB_UNIVERSE_DNS' => [], + 'DB_UNIVERSE_NAME' => [], + 'DB_UNIVERSE_USER' => [], + 'DB_UNIVERSE_PASS' => [], + 'CCP_SSO_URL' => [], + 'CCP_SSO_CLIENT_ID' => [], + 'CCP_SSO_SECRET_KEY' => [], + 'CCP_SSO_DOWNTIME' => [], + 'CCP_ESI_URL' => [], + 'CCP_ESI_DATASOURCE' => [], + 'SMTP_HOST' => [], + 'SMTP_PORT' => [], + 'SMTP_SCHEME' => [], + 'SMTP_USER' => [], + 'SMTP_PASS' => [], + 'SMTP_FROM' => [], + 'SMTP_ERROR' => [] ]; /** @@ -313,12 +313,12 @@ class Setup extends Controller { // obscure some values $obscureVars = ['CCP_SSO_CLIENT_ID', 'CCP_SSO_SECRET_KEY', 'SMTP_PASS']; - foreach($this->environmentVars as $var){ + foreach($this->environmentVars as $var => $options){ if( !in_array($var, $excludeVars) ){ $value = Config::getEnvironmentData($var); $check = true; - if(is_null($value)){ + if(is_null($value) && !array_key_exists('missingOk', $options)){ // variable missing $check = false; $value = '[missing]'; @@ -1557,23 +1557,29 @@ class Setup extends Controller { 'class' => 'txt-color-danger' ]; - $webSocketStatus = [ + $statusWeb = [ 'type' => 'danger', 'label' => 'INIT CONNECTION…', 'class' => 'txt-color-danger' ]; - $statsTcp = [ - 'startup' => 0, - 'connections' => 0, - 'maxConnections' => 0 - ]; + $statsTcp = false; + $statsWeb = false; + + $setStats = function(array $stats) use (&$statsTcp, &$statsWeb) { + if(!empty($stats['tcpSocket'])){ + $statsTcp = $stats['tcpSocket']; + } + if(!empty($stats['webSocket'])){ + $statsWeb = $stats['webSocket']; + } + }; // ping TCP Socket with "healthCheck" task $f3->webSocket(['timeout' => $ttl]) ->write($task, $healthCheckToken) ->then( - function($payload) use ($task, $healthCheckToken, &$statusTcp, &$statsTcp) { + function($payload) use ($task, $healthCheckToken, &$statusTcp, $setStats) { if( $payload['task'] == $task && $payload['load'] == $healthCheckToken @@ -1581,24 +1587,26 @@ class Setup extends Controller { $statusTcp['type'] = 'success'; $statusTcp['label'] = 'PING OK'; $statusTcp['class'] = 'txt-color-success'; - - // statistics (e.g. current connection count) - if(!empty($payload['stats'])){ - $statsTcp = $payload['stats']; - } }else{ $statusTcp['type'] = 'warning'; $statusTcp['label'] = is_string($payload['load']) ? $payload['load'] : 'INVALID RESPONSE'; $statusTcp['class'] = 'txt-color-warning'; } + + // statistics (e.g. current connection count) + $setStats((array)$payload['stats']); }, - function($payload) use (&$statusTcp) { + function($payload) use (&$statusTcp, $setStats) { $statusTcp['label'] = $payload['load']; + + // statistics (e.g. current connection count) + $setStats((array)$payload['stats']); }); $socketInformation = [ 'tcpSocket' => [ - 'label' => 'Socket (intern) [TCP]', + 'label' => 'TCP-Socket (intern)', + 'icon' => 'fa-exchange-alt', 'status' => $statusTcp, 'stats' => $statsTcp, 'data' => [ @@ -1620,15 +1628,17 @@ class Setup extends Controller { 'check' => !empty( $ttl ) ],[ 'label' => 'uptime', - 'value' => Config::formatTimeInterval($statsTcp['startup']), + 'value' => Config::formatTimeInterval($statsTcp['startup'] ? : 0), 'check' => $statsTcp['startup'] > 0 ] ], 'token' => $healthCheckToken ], 'webSocket' => [ - 'label' => 'WebSocket (clients) [HTTP]', - 'status' => $webSocketStatus, + 'label' => 'Web-Socket', + 'icon' => 'fa-random', + 'status' => $statusWeb, + 'stats' => $statsWeb, 'data' => [ [ 'label' => 'URI', diff --git a/app/main/cron/characterupdate.php b/app/main/cron/characterupdate.php index 325b1339..b1cb2055 100644 --- a/app/main/cron/characterupdate.php +++ b/app/main/cron/characterupdate.php @@ -64,10 +64,15 @@ class CharacterUpdate extends AbstractCron { * @var $characterLog Pathfinder\CharacterLogModel */ if(is_object($characterLog->characterId)){ - // force characterLog as "updated" even if no changes were made - $characterLog->characterId->updateLog([ - 'markUpdated' => true - ]); + if($accessToken = $characterLog->characterId->getAccessToken()){ + if($this->isOnline($accessToken)){ + // force characterLog as "updated" even if no changes were made + $characterLog->touch('updated'); + $characterLog->save(); + }else{ + $characterLog->erase(); + } + } }else{ // character_log does not have a character assigned -> delete $characterLog->erase(); diff --git a/app/main/cron/maphistory.php b/app/main/cron/maphistory.php index 330c6a58..c9eabd0d 100644 --- a/app/main/cron/maphistory.php +++ b/app/main/cron/maphistory.php @@ -14,7 +14,10 @@ use data\filesystem\Search; class MapHistory extends AbstractCron { - const LOG_TEXT = '%s [%4s] log files, [%4s] not writable, [%4s] read error, [%4s] write error, [%4s] rename error, [%4s] delete error, exec (%.3Fs)'; + /** + * log msg for truncateFiles() cronjob + */ + const LOG_TEXT = '%s [%4s] log files, [%s] truncated, [%4s] not writable, [%4s] read error, [%4s] write error, [%4s] rename error, [%4s] delete error, exec (%.3Fs)'; /** * default log file size limit before truncate, bytes (1MB) @@ -65,6 +68,7 @@ class MapHistory extends AbstractCron { $writeErrors = 0; $renameErrors = 0; $deleteErrors = 0; + $truncatedFileNames = []; if($f3->exists('PATHFINDER.HISTORY.LOG', $dir)){ $fileHandler = FileHandler::instance(); @@ -98,7 +102,8 @@ class MapHistory extends AbstractCron { // move temp file from PHP temp dir into Pathfinders history log dir... // ... overwrite old log file with new file if(rename($temp, $file->getRealPath())){ - // map history logs should be writable non cronjob user too + $truncatedFileNames[] = $file->getFilename(); + // map history logs should be writable for non cronjob user too @chmod($file->getRealPath(), 0666); }else{ $renameErrors++; @@ -120,6 +125,6 @@ class MapHistory extends AbstractCron { // Log ------------------------ $log = new \Log('cron_' . __FUNCTION__ . '.log'); - $log->write(sprintf(self::LOG_TEXT, __FUNCTION__, $largeFiles, $notWritableFiles, $readErrors, $writeErrors, $renameErrors, $deleteErrors, $execTime)); + $log->write(sprintf(self::LOG_TEXT, __FUNCTION__, $largeFiles, implode(', ', $truncatedFileNames), $notWritableFiles, $readErrors, $writeErrors, $renameErrors, $deleteErrors, $execTime)); } } \ No newline at end of file diff --git a/app/main/lib/config.php b/app/main/lib/config.php index d1a7698a..1ce9ee9d 100644 --- a/app/main/lib/config.php +++ b/app/main/lib/config.php @@ -301,7 +301,7 @@ class Config extends \Prefab { 'PASS' => self::getEnvironmentData('DB_' . $alias . '_PASS') ]; - $pdoReg = '/^(?[[:alpha:]]+):((host=(?[a-zA-Z0-9\.]*))|(unix_socket=(?[a-zA-Z0-9\/]*\.sock)))((;dbname=(?\w*))|(;port=(?\d*))){0,2}/'; + $pdoReg = '/^(?[[:alpha:]]+):((host=(?[a-zA-Z0-9-_\.]*))|(unix_socket=(?[a-zA-Z0-9\/]*\.sock)))((;dbname=(?\w*))|(;port=(?\d*))){0,2}/'; if(preg_match($pdoReg, self::getEnvironmentData('DB_' . $alias . '_DNS'), $matches)){ // remove unnamed matches $matches = array_intersect_key($matches, $config); diff --git a/app/main/lib/db/SQL.php b/app/main/lib/db/SQL.php index dc56edba..be085338 100644 --- a/app/main/lib/db/SQL.php +++ b/app/main/lib/db/SQL.php @@ -76,4 +76,17 @@ class SQL extends \DB\SQL { ); } } + + /** + * @see https://fatfreeframework.com/3.6/sql#exec + * @param array|string $cmds + * @param null $args + * @param int $ttl + * @param bool $log (we use false as default parameter) + * @param bool $stamp + * @return array|FALSE|int + */ + function exec($cmds, $args = null, $ttl = 0, $log = false, $stamp = false) { + return parent::exec($cmds, $args, $ttl, $log, $stamp); + } } \ No newline at end of file diff --git a/app/main/lib/logging/AbstractLog.php b/app/main/lib/logging/AbstractLog.php index c0e804de..6ad4c607 100644 --- a/app/main/lib/logging/AbstractLog.php +++ b/app/main/lib/logging/AbstractLog.php @@ -116,7 +116,7 @@ abstract class AbstractLog implements LogInterface { // add custom log processor callback -> add "extra" (meta) data $f3 = $this->f3; - $processorExtraData = function($record) use(&$f3){ + $processorExtraData = function($record) use (&$f3){ $record['extra'] = [ 'path' => $f3->get('PATH'), 'ip' => $f3->get('IP') diff --git a/app/main/lib/resource.php b/app/main/lib/resource.php index 3324a143..7895129c 100644 --- a/app/main/lib/resource.php +++ b/app/main/lib/resource.php @@ -24,6 +24,7 @@ class Resource extends \Prefab { 'style' => 'style', 'script' => 'script', 'font' => 'font', + 'document' => 'document', 'image' => 'image' ]; @@ -49,6 +50,7 @@ class Resource extends \Prefab { 'style' => '', 'script' => '', 'font' => '', + 'document' => '', 'image' => '' ]; @@ -60,6 +62,7 @@ class Resource extends \Prefab { private $fileExt = [ 'style' => 'css', 'script' => 'js', + 'document' => 'html', 'font' => 'woff2' ]; diff --git a/app/main/lib/socket/AbstractSocket.php b/app/main/lib/socket/AbstractSocket.php index f39c9804..90c79419 100644 --- a/app/main/lib/socket/AbstractSocket.php +++ b/app/main/lib/socket/AbstractSocket.php @@ -17,6 +17,12 @@ use Clue\React\NDJson; abstract class AbstractSocket implements SocketInterface { + /** + * max length for JSON data string + * -> throw OverflowException on exceed + */ + const JSON_DECODE_MAX_LENGTH = 65536 * 4; + /** * @var EventLoop\LoopInterface|null */ @@ -195,7 +201,7 @@ abstract class AbstractSocket implements SocketInterface { // new empty stream for processing JSON $stream = new Stream\ThroughStream(); - $streamDecoded = new NDJson\Decoder($stream, true); + $streamDecoded = new NDJson\Decoder($stream, true, 512, 0, self::JSON_DECODE_MAX_LENGTH); // promise get resolved on first emit('data') $promise = Promise\Stream\first($streamDecoded); diff --git a/app/main/model/abstractmodel.php b/app/main/model/abstractmodel.php index db5e8594..7102f198 100644 --- a/app/main/model/abstractmodel.php +++ b/app/main/model/abstractmodel.php @@ -9,6 +9,7 @@ namespace Model; use DB\Cortex; +use DB\CortexCollection; use DB\SQL\Schema; use lib\logging; use Controller; @@ -330,6 +331,13 @@ abstract class AbstractModel extends Cortex { $valid = true; } break; + case Schema::DT_VARCHAR128: + case Schema::DT_VARCHAR256: + case Schema::DT_VARCHAR512: + if(!empty($val)){ + $valid = true; + } + break; default: } } @@ -542,7 +550,34 @@ abstract class AbstractModel extends Cortex { /** * @var $relModel self|bool */ - $relModel = $this->rel($key)->findone($this->mergeFilter([$this->mergeWithRelFilter($key, $filter), $relFilter])); + $relModel = $this->rel($key)->findone($this->mergeFilter([$relFilter, $this->mergeWithRelFilter($key, $filter)])); + } + + return $relModel ? : null; + } + + /** + * get all models from a relation that match $filter + * @param string $key + * @param array $filter + * @return CortexCollection|null + */ + protected function relFind(string $key, array $filter) : ?CortexCollection { + $relModel = null; + $relFilter = []; + if($this->exists($key, true)){ + $fieldConf = $this->getFieldConfiguration(); + if(array_key_exists($key, $fieldConf)){ + if(array_key_exists($type = 'has-many', $fieldConf[$key])){ + $fromConf = $fieldConf[$key][$type]; + $relFilter = self::getFilter($fromConf[1], $this->getRaw($fromConf['relField'])); + } + } + + /** + * @var $relModel CortexCollection|bool + */ + $relModel = $this->rel($key)->find($this->mergeFilter([$relFilter, $this->mergeWithRelFilter($key, $filter)])); } return $relModel ? : null; @@ -862,13 +897,18 @@ abstract class AbstractModel extends Cortex { } /** - * get filter for Cortex + * get new filter array representation + * -> $suffix can be used fore unique placeholder, + * in case the same $key is used with different $values in the same query * @param string $key - * @param $value + * @param mixed $value + * @param string $operator + * @param string $suffix * @return array */ - public static function getFilter(string $key, $value) : array { - return [$key . ' = :' . $key, ':' . $key => $value]; + public static function getFilter(string $key, $value, string $operator = '=', string $suffix = '') : array { + $placeholder = ':' . implode('_', array_filter([$key, $suffix])); + return [$key . ' ' . $operator . ' ' . $placeholder, $placeholder => $value]; } /** diff --git a/app/main/model/pathfinder/characterlogmodel.php b/app/main/model/pathfinder/characterlogmodel.php index 2394e650..8796ae93 100644 --- a/app/main/model/pathfinder/characterlogmodel.php +++ b/app/main/model/pathfinder/characterlogmodel.php @@ -8,14 +8,19 @@ namespace Model\Pathfinder; -use Controller\Api\User as User; -use Controller\Controller as Controller; + use DB\SQL\Schema; class CharacterLogModel extends AbstractPathfinderModel { + /** + * @var string + */ protected $table = 'character_log'; + /** + * @var array + */ protected $fieldConf = [ 'active' => [ 'type' => Schema::DT_BOOL, @@ -38,32 +43,37 @@ class CharacterLogModel extends AbstractPathfinderModel { 'systemId' => [ 'type' => Schema::DT_INT, 'index' => true, - 'activity-log' => true + 'activity-log' => true, + 'validate' => 'notEmpty' ], 'systemName' => [ 'type' => Schema::DT_VARCHAR128, 'nullable' => false, - 'default' => '' + 'default' => '', + 'activity-log' => true, + 'validate' => 'notEmpty' ], 'shipTypeId' => [ 'type' => Schema::DT_INT, 'index' => true, - 'activity-log' => true + 'activity-log' => true ], 'shipTypeName' => [ 'type' => Schema::DT_VARCHAR128, 'nullable' => false, - 'default' => '' + 'default' => '', + 'activity-log' => true ], 'shipId' => [ 'type' => Schema::DT_BIGINT, 'index' => true, - 'activity-log' => true + 'activity-log' => true ], 'shipMass' => [ 'type' => Schema::DT_FLOAT, 'nullable' => false, - 'default' => 0 + 'default' => 0, + 'activity-log' => true ], 'shipName' => [ 'type' => Schema::DT_VARCHAR128, @@ -73,22 +83,24 @@ class CharacterLogModel extends AbstractPathfinderModel { 'stationId' => [ 'type' => Schema::DT_INT, 'index' => true, - 'activity-log' => true + 'activity-log' => true ], 'stationName' => [ 'type' => Schema::DT_VARCHAR128, 'nullable' => false, - 'default' => '' + 'default' => '', + 'activity-log' => true ], 'structureId' => [ 'type' => Schema::DT_BIGINT, 'index' => true, - 'activity-log' => true + 'activity-log' => true ], 'structureName' => [ 'type' => Schema::DT_VARCHAR128, 'nullable' => false, - 'default' => '' + 'default' => '', + 'activity-log' => true ] ]; @@ -139,10 +151,10 @@ class CharacterLogModel extends AbstractPathfinderModel { } /** - * get all character log data - * @return object + * get character log data + * @return \stdClass */ - public function getData(){ + public function getData() : \stdClass { $logData = (object) []; $logData->system = (object) []; @@ -168,16 +180,11 @@ class CharacterLogModel extends AbstractPathfinderModel { } /** - * setter for systemId - * @param int $systemId - * @return int - * @throws \Exception + * get 'character log' data as array + * @return array */ - public function set_systemId($systemId){ - if($systemId > 0){ - $this->updateCharacterSessionLocation($systemId); - } - return $systemId; + public function getDataAsArray() : array { + return json_decode(json_encode($this->getData()), true); } /** @@ -197,6 +204,8 @@ class CharacterLogModel extends AbstractPathfinderModel { * @param $pkeys */ public function afterUpdateEvent($self, $pkeys){ + $self->updateLogsHistory('update'); + // check if any "relevant" column has changed if(!empty($this->fieldChanges)){ $self->clearCacheData(); @@ -210,6 +219,7 @@ class CharacterLogModel extends AbstractPathfinderModel { * @param $pkeys */ public function afterEraseEvent($self, $pkeys){ + $self->deleteLogsHistory(); $self->clearCacheData(); } @@ -229,30 +239,40 @@ class CharacterLogModel extends AbstractPathfinderModel { } /** - * update session data for active character - * @param int $systemId - * @throws \Exception + * update 'character log' history data + * -> checks $this->fieldChanges + * @param string $action */ - protected function updateCharacterSessionLocation(int $systemId){ - $controller = new Controller(); - + protected function updateLogsHistory(string $action){ + // add new log history entry if 'systemId' changed + // -> if e.g. 'shipTypeId', 'stationId',.. changed -> no new entry (for now) if( - !empty($sessionCharacter = $controller->getSessionCharacterData()) && - $sessionCharacter['ID'] === $this->get('characterId', true) + !empty($this->fieldChanges) && + array_key_exists('systemId', $this->fieldChanges) && // new history entry + is_object($this->characterId) ){ - $systemChanged = false; - if((int)$sessionCharacter['PREV_SYSTEM_ID'] === 0){ - $sessionCharacter['PREV_SYSTEM_ID'] = (int)$systemId; - $systemChanged = true; - }elseif((int)$sessionCharacter['PREV_SYSTEM_ID'] !== $this->systemId){ - $sessionCharacter['PREV_SYSTEM_ID'] = $this->systemId; - $systemChanged = true; - } + $oldLog = clone $this; - if($systemChanged){ - $sessionCharacters = CharacterModel::mergeSessionCharacterData([$sessionCharacter]); - $this->getF3()->set(User::SESSION_KEY_CHARACTERS, $sessionCharacters); + // get 'updated' timestamp and reapply after __set() fields data + // -> because any __set() call updates 'updated' col + $updated = $oldLog->updated; + foreach($this->fieldChanges as $key => $change){ + if($oldLog->exists($key)){ + $oldLog->$key = $change['old']; + } } + $oldLog->updated = $updated; + + $oldLog->characterId->updateLogsHistory($oldLog, $action); + } + } + + /** + * delete 'character log' history data + */ + protected function deleteLogsHistory(){ + if(is_object($this->characterId)){ + $this->characterId->clearCacheDataWithPrefix(CharacterModel::DATA_CACHE_KEY_LOG_HISTORY); } } diff --git a/app/main/model/pathfinder/charactermodel.php b/app/main/model/pathfinder/charactermodel.php index 8fd7d099..768cf71c 100644 --- a/app/main/model/pathfinder/charactermodel.php +++ b/app/main/model/pathfinder/charactermodel.php @@ -19,17 +19,32 @@ class CharacterModel extends AbstractPathfinderModel { /** * @var string */ - protected $table = 'character'; + protected $table = 'character'; /** * cache key prefix for getData(); result WITH log data */ - const DATA_CACHE_KEY_LOG = 'LOG'; + const DATA_CACHE_KEY_LOG = 'LOG'; /** * log message for character access */ - const LOG_ACCESS = 'charId: [%20s], status: %s, charName: %s'; + const LOG_ACCESS = 'charId: [%20s], status: %s, charName: %s'; + + /** + * max count of historic character logs + */ + const MAX_LOG_HISTORY_DATA = 5; + + /** + * TTL for historic character logs + */ + const TTL_LOG_HISTORY = 60 * 60 * 22; + + /** + * cache key prefix historic character logs + */ + const DATA_CACHE_KEY_LOG_HISTORY = 'LOG_HISTORY'; /** * character authorization status @@ -186,56 +201,60 @@ class CharacterModel extends AbstractPathfinderModel { /** * get character data - * @param bool|false $addCharacterLogData - * @return null|object|\stdClass + * @param bool $addLogData + * @param bool $addLogHistoryData + * @return mixed|object|null * @throws \Exception */ - public function getData($addCharacterLogData = false){ - $cacheKeyModifier = ''; - - // check if there is cached data - // -> IMPORTANT: $addCharacterLogData is optional! -> therefore we need 2 cache keys! - if($addCharacterLogData){ - $cacheKeyModifier = self::DATA_CACHE_KEY_LOG; - } - $characterData = $this->getCacheData($cacheKeyModifier); - - if(is_null($characterData)){ + public function getData($addLogData = false, $addLogHistoryData = false){ + // check for cached data + if(is_null($characterData = $this->getCacheData())){ // no cached character data found - $characterData = (object) []; - $characterData->id = $this->_id; - $characterData->name = $this->name; - $characterData->role = $this->roleId->getData(); - $characterData->shared = $this->shared; - $characterData->logLocation = $this->logLocation; - $characterData->selectLocation = $this->selectLocation; - - if($addCharacterLogData){ - if($logModel = $this->getLog()){ - $characterData->log = $logModel->getData(); - } - } + $characterData = (object) []; + $characterData->id = $this->_id; + $characterData->name = $this->name; + $characterData->role = $this->roleId->getData(); + $characterData->shared = $this->shared; + $characterData->logLocation = $this->logLocation; + $characterData->selectLocation = $this->selectLocation; // check for corporation if($corporation = $this->getCorporation()){ - $characterData->corporation = $corporation->getData(); + $characterData->corporation = $corporation->getData(); } // check for alliance if($alliance = $this->getAlliance()){ - $characterData->alliance = $alliance->getData(); + $characterData->alliance = $alliance->getData(); } // max caching time for a system - // the cached date has to be cleared manually on any change - // this includes system, connection,... changes (all dependencies) - $this->updateCacheData($characterData, $cacheKeyModifier); + // cached date has to be cleared manually on any change + // this applies to system, connection,... changes (+ all other dependencies) + $this->updateCacheData($characterData); + } + + if($addLogData){ + if(is_null($logData = $this->getCacheData(self::DATA_CACHE_KEY_LOG))){ + if($logModel = $this->getLog()){ + $logData = $logModel->getData(); + $this->updateCacheData($logData, self::DATA_CACHE_KEY_LOG); + } + } + + if($logData){ + $characterData->log = $logData; + } + } + + if($addLogHistoryData && $characterData->log){ + $characterData->logHistory = $this->getLogsHistory(); } // temp "authStatus" should not be cached if($this->authStatus){ - $characterData->authStatus = $this->authStatus; + $characterData->authStatus = $this->authStatus; } return $characterData; @@ -507,7 +526,7 @@ class CharacterModel extends AbstractPathfinderModel { /** * get ESI API "access_token" from OAuth - * @return bool|mixed + * @return bool|string */ public function getAccessToken(){ $accessToken = false; @@ -797,6 +816,31 @@ class CharacterModel extends AbstractPathfinderModel { $this->roleId = $this->requestRole(); } + /** + * get online status data from ESI + * @param string $accessToken + * @return array + */ + protected function getOnlineData(string $accessToken) : array { + return self::getF3()->ccpClient()->getCharacterOnlineData($this->_id, $accessToken); + } + + /** + * check online state from ESI + * @param string $accessToken + * @return bool + */ + public function isOnline(string $accessToken) : bool { + $isOnline = false; + $onlineData = $this->getOnlineData($accessToken); + + if($onlineData['online'] === true){ + $isOnline = true; + } + + return $isOnline; + } + /** * update character log (active system, ...) * -> API request for character log data @@ -815,170 +859,162 @@ class CharacterModel extends AbstractPathfinderModel { $this->hasBasicScopes() ){ // Try to pull data from API - if( $accessToken = $this->getAccessToken() ){ - $onlineData = self::getF3()->ccpClient()->getCharacterOnlineData($this->_id, $accessToken); + if($accessToken = $this->getAccessToken()){ + if($this->isOnline($accessToken)){ + $locationData = self::getF3()->ccpClient()->getCharacterLocationData($this->_id, $accessToken); - // check whether character is currently ingame online - if(is_bool($onlineData['online'])){ - if($onlineData['online'] === true){ - $locationData = self::getF3()->ccpClient()->getCharacterLocationData($this->_id, $accessToken); + if( !empty($locationData['system']['id']) ){ + // character is currently in-game - if( !empty($locationData['system']['id']) ){ - // character is currently in-game + // get current $characterLog or get new --------------------------------------------------- + if( !($characterLog = $this->getLog()) ){ + // create new log + $characterLog = $this->rel('characterLog'); + } - // get current $characterLog or get new --------------------------------------------------- - if( !($characterLog = $this->getLog()) ){ - // create new log - $characterLog = $this->rel('characterLog'); - } + // get current log data and modify on change + $logData = $characterLog->getDataAsArray(); - // get current log data and modify on change - $logData = json_decode(json_encode( $characterLog->getData()), true); + // check system and station data for changes ---------------------------------------------- - // check system and station data for changes ---------------------------------------------- + // IDs for "systemId", "stationId" that require more data + $lookupUniverseIds = []; - // IDs for "systemId", "stationId" that require more data - $lookupUniverseIds = []; + if( + empty($logData['system']['name']) || + $logData['system']['id'] !== $locationData['system']['id'] + ){ + // system changed -> request "system name" for current system + $lookupUniverseIds[] = $locationData['system']['id']; + } + if( !empty($locationData['station']['id']) ){ if( - empty($logData['system']['name']) || - $logData['system']['id'] !== $locationData['system']['id'] + empty($logData['station']['name']) || + $logData['station']['id'] !== $locationData['station']['id'] ){ - // system changed -> request "system name" for current system - $lookupUniverseIds[] = $locationData['system']['id']; + // station changed -> request "station name" for current station + $lookupUniverseIds[] = $locationData['station']['id']; } + }else{ + unset($logData['station']); + } - if( !empty($locationData['station']['id']) ){ + $logData = array_replace_recursive($logData, $locationData); + + // get "more" data for systemId and/or stationId ----------------------------------------- + if( !empty($lookupUniverseIds) ){ + // get "more" information for some Ids (e.g. name) + $universeData = self::getF3()->ccpClient()->getUniverseNamesData($lookupUniverseIds); + + if( !empty($universeData) && !isset($universeData['error']) ){ + // We expect max ONE system AND/OR station data, not an array of e.g. systems + if(!empty($universeData['system'])){ + $universeData['system'] = reset($universeData['system']); + } + if(!empty($universeData['station'])){ + $universeData['station'] = reset($universeData['station']); + } + + $logData = array_replace_recursive($logData, $universeData); + }else{ + // this is important! universe data is a MUST HAVE! + $deleteLog = true; + } + } + + // check structure data for changes ------------------------------------------------------- + if(!$deleteLog){ + + // IDs for "structureId" that require more data + $lookupStructureId = 0; + if( !empty($locationData['structure']['id']) ){ if( - empty($logData['station']['name']) || - $logData['station']['id'] !== $locationData['station']['id'] + empty($logData['structure']['name']) || + $logData['structure']['id'] !== $locationData['structure']['id'] ){ - // station changed -> request "station name" for current station - $lookupUniverseIds[] = $locationData['station']['id']; + // structure changed -> request "structure name" for current station + $lookupStructureId = $locationData['structure']['id']; } }else{ - unset($logData['station']); + unset($logData['structure']); } - $logData = array_replace_recursive($logData, $locationData); - - // get "more" data for systemId and/or stationId ----------------------------------------- - if( !empty($lookupUniverseIds) ){ - // get "more" information for some Ids (e.g. name) - $universeData = self::getF3()->ccpClient()->getUniverseNamesData($lookupUniverseIds); - - if( !empty($universeData) && !isset($universeData['error']) ){ - // We expect max ONE system AND/OR station data, not an array of e.g. systems - if(!empty($universeData['system'])){ - $universeData['system'] = reset($universeData['system']); - } - if(!empty($universeData['station'])){ - $universeData['station'] = reset($universeData['station']); - } - - $logData = array_replace_recursive($logData, $universeData); - }else{ - // this is important! universe data is a MUST HAVE! - $deleteLog = true; - } - } - - // check structure data for changes ------------------------------------------------------- - if(!$deleteLog){ - - // IDs for "structureId" that require more data - $lookupStructureId = 0; - if( !empty($locationData['structure']['id']) ){ - if( - empty($logData['structure']['name']) || - $logData['structure']['id'] !== $locationData['structure']['id'] - ){ - // structure changed -> request "structure name" for current station - $lookupStructureId = $locationData['structure']['id']; - } + // get "more" data for structureId --------------------------------------------------- + if($lookupStructureId > 0){ + /** + * @var $structureModel Universe\StructureModel + */ + $structureModel = Universe\AbstractUniverseModel::getNew('StructureModel'); + $structureModel->loadById($lookupStructureId, $accessToken, $additionalOptions); + if(!$structureModel->dry()){ + $structureData['structure'] = (array)$structureModel->getData(); + $logData = array_replace_recursive($logData, $structureData); }else{ unset($logData['structure']); } + } + } - // get "more" data for structureId --------------------------------------------------- - if($lookupStructureId > 0){ - /** - * @var $structureModel Universe\StructureModel - */ - $structureModel = Universe\AbstractUniverseModel::getNew('StructureModel'); - $structureModel->loadById($lookupStructureId, $accessToken, $additionalOptions); - if(!$structureModel->dry()){ - $structureData['structure'] = (array)$structureModel->getData(); - $logData = array_replace_recursive($logData, $structureData); - }else{ - unset($logData['structure']); - } + // check ship data for changes ------------------------------------------------------------ + if( !$deleteLog ){ + $shipData = self::getF3()->ccpClient()->getCharacterShipData($this->_id, $accessToken); + + // IDs for "shipTypeId" that require more data + $lookupShipTypeId = 0; + if( !empty($shipData['ship']['typeId']) ){ + if( + empty($logData['ship']['typeName']) || + $logData['ship']['typeId'] !== $shipData['ship']['typeId'] + ){ + // ship changed -> request "station name" for current station + $lookupShipTypeId = $shipData['ship']['typeId']; } + + // "shipName"/"shipId" could have changed... + $logData = array_replace_recursive($logData, $shipData); + }else{ + // ship data should never be empty -> keep current one + //unset($logData['ship']); + $invalidResponse = true; } - // check ship data for changes ------------------------------------------------------------ - if( !$deleteLog ){ - $shipData = self::getF3()->ccpClient()->getCharacterShipData($this->_id, $accessToken); - - // IDs for "shipTypeId" that require more data - $lookupShipTypeId = 0; - if( !empty($shipData['ship']['typeId']) ){ - if( - empty($logData['ship']['typeName']) || - $logData['ship']['typeId'] !== $shipData['ship']['typeId'] - ){ - // ship changed -> request "station name" for current station - $lookupShipTypeId = $shipData['ship']['typeId']; - } - - // "shipName"/"shipId" could have changed... + // get "more" data for shipTypeId ---------------------------------------------------- + if($lookupShipTypeId > 0){ + /** + * @var $typeModel Universe\TypeModel + */ + $typeModel = Universe\AbstractUniverseModel::getNew('TypeModel'); + $typeModel->loadById($lookupShipTypeId, '', $additionalOptions); + if(!$typeModel->dry()){ + $shipData['ship'] = (array)$typeModel->getShipData(); $logData = array_replace_recursive($logData, $shipData); }else{ - // ship data should never be empty -> keep current one - //unset($logData['ship']); - $invalidResponse = true; - } - - // get "more" data for shipTypeId ---------------------------------------------------- - if($lookupShipTypeId > 0){ - /** - * @var $typeModel Universe\TypeModel - */ - $typeModel = Universe\AbstractUniverseModel::getNew('TypeModel'); - $typeModel->loadById($lookupShipTypeId, '', $additionalOptions); - if(!$typeModel->dry()){ - $shipData['ship'] = (array)$typeModel->getShipData(); - $logData = array_replace_recursive($logData, $shipData); - }else{ - // this is important! ship data is a MUST HAVE! - $deleteLog = true; - } + // this is important! ship data is a MUST HAVE! + $deleteLog = true; } } + } - if( !$deleteLog ){ - // mark log as "updated" even if no changes were made - if($additionalOptions['markUpdated'] === true){ - $characterLog->touch('updated'); - } - - $characterLog->setData($logData); - $characterLog->characterId = $this->id; - $characterLog->save(); - - $this->characterLog = $characterLog; + if( !$deleteLog ){ + // mark log as "updated" even if no changes were made + if($additionalOptions['markUpdated'] === true){ + $characterLog->touch('updated'); } - }else{ - // systemId should always exists - $invalidResponse = true; + + $characterLog->setData($logData); + $characterLog->characterId = $this->_id; + $characterLog->save(); + + $this->characterLog = $characterLog; } }else{ - // user is in-game offline - $deleteLog = true; + // systemId should always exists + $invalidResponse = true; } }else{ - // online status request failed - $invalidResponse = true; + // user is in-game offline + $deleteLog = true; } }else{ // access token request failed @@ -996,6 +1032,51 @@ class CharacterModel extends AbstractPathfinderModel { return $this; } + /** + * filter'character log' history data by $callback + * @param \Closure $callback + * @return array + */ + protected function filterLogsHistory(\Closure $callback) : array { + return array_filter($this->getLogsHistory() , $callback); + } + + /** + * @return array + */ + public function getLogsHistory() : array { + if(!is_array($logHistoryData = $this->getCacheData(self::DATA_CACHE_KEY_LOG_HISTORY))){ + $logHistoryData = []; + } + return $logHistoryData; + } + + /** + * add new 'character log' history entry + * @param CharacterLogModel $characterLog + * @param string $action + */ + public function updateLogsHistory(CharacterLogModel $characterLog, string $action = 'update'){ + if( + !$this->dry() && + $this->_id === $characterLog->get('characterId', true) + ){ + $logHistoryData = $this->getLogsHistory(); + $historyEntry = [ + 'stamp' => strtotime($characterLog->updated), + 'action' => $action, + 'log' => $characterLog->getDataAsArray() + ]; + + array_unshift($logHistoryData, $historyEntry); + + // limit max history data + array_splice($logHistoryData, self::MAX_LOG_HISTORY_DATA); + + $this->updateCacheData($logHistoryData, self::DATA_CACHE_KEY_LOG_HISTORY, self::TTL_LOG_HISTORY); + } + } + /** * broadcast characterData */ @@ -1055,15 +1136,33 @@ class CharacterModel extends AbstractPathfinderModel { /** * get the character log entry for this character - * @return bool|CharacterLogModel + * @return CharacterLogModel|null */ - public function getLog(){ - $characterLog = false; - if( - $this->hasLog() && - !$this->characterLog->dry() - ){ - $characterLog = $this->characterLog; + public function getLog() : ?CharacterLogModel { + return ($this->hasLog() && !$this->characterLog->dry()) ? $this->characterLog : null; + } + + /** + * get the first matched (most recent) log entry before $systemId. + * -> The returned log entry *might* be previous system for this character + * @param int $systemId + * @return CharacterLogModel|null + */ + public function getLogPrevSystem(int $systemId) : ?CharacterLogModel { + $logHistoryData = $this->filterLogsHistory(function(array $historyEntry) use ($systemId) : bool { + return ( + !empty($historySystemId = (int)$historyEntry['log']['system']['id']) && + $historySystemId !== $systemId + ); + }); + + $characterLog = null; + if(!empty($historyEntry = reset($logHistoryData))){ + /** + * @var $characterLog CharacterLogModel + */ + $characterLog = $this->rel('characterLog'); + $characterLog->setData($historyEntry['log']); } return $characterLog; diff --git a/app/main/model/pathfinder/connectionmodel.php b/app/main/model/pathfinder/connectionmodel.php index 3f29cda3..6f361c6d 100644 --- a/app/main/model/pathfinder/connectionmodel.php +++ b/app/main/model/pathfinder/connectionmodel.php @@ -88,6 +88,29 @@ class ConnectionModel extends AbstractMapTrackingModel { ] ]; + /** + * allowed connection types + * @var array + */ + protected static $connectionTypeWhitelist = [ + // base type for scopes + 'abyssal', + 'jumpbridge', + 'stargate', + // wh mass reduction types + 'wh_fresh', + 'wh_reduced', + 'wh_critical', + // wh jump mass types + 'wh_jump_mass_s', + 'wh_jump_mass_m', + 'wh_jump_mass_l', + 'wh_jump_mass_xl', + // other types + 'wh_eol', + 'preserve_mass' + ]; + /** * get connection data * @param bool $addSignatureData @@ -130,13 +153,15 @@ class ConnectionModel extends AbstractMapTrackingModel { * @return int|number */ public function set_type($type){ - $newTypes = (array)$type; + // remove unwanted types -> they should not be send from client + // -> reset keys! otherwise JSON format results in object and not in array + $type = array_values(array_intersect(array_unique((array)$type), self::$connectionTypeWhitelist)); // set EOL timestamp - if( !in_array('wh_eol', $newTypes) ){ + if( !in_array('wh_eol', $type) ){ $this->eolUpdated = null; }elseif( - in_array('wh_eol', $newTypes) && + in_array('wh_eol', $type) && !in_array('wh_eol', (array)$this->type) // $this->type == null for new connection! (e.g. map import) ){ // connection EOL status change @@ -307,35 +332,31 @@ class ConnectionModel extends AbstractMapTrackingModel { * @return logging\LogInterface * @throws \Exception\ConfigException */ - public function newLog($action = '') : Logging\LogInterface { + public function newLog(string $action = '') : Logging\LogInterface { return $this->getMap()->newLog($action)->setTempData($this->getLogObjectData()); } /** * @return MapModel */ - public function getMap() : MapModel{ + public function getMap() : MapModel { return $this->get('mapId'); } /** * delete a connection * @param CharacterModel $characterModel + * @return bool */ - public function delete(CharacterModel $characterModel){ - if( !$this->dry() ){ - // check if character has access - if($this->hasAccess($characterModel)){ - $this->erase(); - } - } + public function delete(CharacterModel $characterModel) : bool { + return ($this->valid() && $this->hasAccess($characterModel)) ? $this->erase() : false; } /** * get object relevant data for model log * @return array */ - public function getLogObjectData() : array{ + public function getLogObjectData() : array { return [ 'objId' => $this->_id, 'objName' => $this->scope diff --git a/app/main/model/pathfinder/corporationmodel.php b/app/main/model/pathfinder/corporationmodel.php index c6fbf478..d464411d 100644 --- a/app/main/model/pathfinder/corporationmodel.php +++ b/app/main/model/pathfinder/corporationmodel.php @@ -185,7 +185,7 @@ class CorporationModel extends AbstractPathfinderModel { * @param array $options * @return array */ - public function getMaps($mapIds = [], $options = []){ + public function getMaps($mapIds = [], $options = []) : array { $maps = []; $this->filterRel(); diff --git a/app/main/model/pathfinder/mapmodel.php b/app/main/model/pathfinder/mapmodel.php index b3209f3a..e40ce578 100644 --- a/app/main/model/pathfinder/mapmodel.php +++ b/app/main/model/pathfinder/mapmodel.php @@ -8,6 +8,7 @@ namespace Model\Pathfinder; +use DB\CortexCollection; use DB\SQL\Schema; use data\file\FileHandler; use Exception\ConfigException; @@ -91,6 +92,12 @@ class MapModel extends AbstractMapTrackingModel { 'default' => 1, 'activity-log' => true ], + 'persistentSignatures' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'activity-log' => true + ], 'logActivity' => [ 'type' => Schema::DT_BOOL, 'nullable' => false, @@ -215,6 +222,7 @@ class MapModel extends AbstractMapTrackingModel { $mapData->deleteExpiredConnections = $this->deleteExpiredConnections; $mapData->deleteEolConnections = $this->deleteEolConnections; $mapData->persistentAliases = $this->persistentAliases; + $mapData->persistentSignatures = $this->persistentSignatures; // map scope $mapData->scope = (object) []; @@ -614,36 +622,27 @@ class MapModel extends AbstractMapTrackingModel { } /** - * get all connections in this map + * get connections in this map + * -> $connectionIds can be used for filter * @param null $connectionIds * @param string $scope - * @return ConnectionModel[] + * @return CortexCollection|array */ public function getConnections($connectionIds = null, $scope = ''){ - $connections = []; - - $query = [ - 'active = :active AND source > 0 AND target > 0', - ':active' => 1 + $filters = [ + self::getFilter('source', 0, '>'), + self::getFilter('target', 0, '>') ]; if(!empty($scope)){ - $query[0] .= ' AND scope = :scope'; - $query[':scope'] = $scope; + $filters[] = self::getFilter('scope', $scope); } if(!empty($connectionIds)){ - $query[0] .= ' AND id IN (?)'; - $query[] = $connectionIds; + $filters[] = self::getFilter('id', $connectionIds, 'IN'); } - $this->filter('connections', $query); - - if($this->connections){ - $connections = $this->connections; - } - - return $connections; + return $this->relFind('connections', $this->mergeFilter($filters)) ? : []; } /** @@ -658,7 +657,7 @@ class MapModel extends AbstractMapTrackingModel { /** * @var $connection ConnectionModel */ - $connectionData[] = $connection->getData(); + $connectionData[] = $connection->getData(true); } return $connectionData; @@ -798,12 +797,11 @@ class MapModel extends AbstractMapTrackingModel { public function hasAccess(CharacterModel $characterModel) : bool { $hasAccess = false; - if( !$this->dry() ){ + if($this->valid()){ // get all maps the user has access to // this includes corporation and alliance maps - $maps = $characterModel->getMaps(); - foreach($maps as $map){ - if($map->id === $this->id){ + foreach($characterModel->getMaps() as $map){ + if($map->_id === $this->_id){ $hasAccess = true; break; } @@ -977,7 +975,7 @@ class MapModel extends AbstractMapTrackingModel { * @return logging\LogInterface * @throws ConfigException */ - public function newLog($action = '') : Logging\LogInterface{ + public function newLog(string $action = '') : Logging\LogInterface{ $logChannelData = $this->getLogChannelData(); $logObjectData = $this->getLogObjectData(); $log = (new logging\MapLog($action, $logChannelData))->setTempData($logObjectData); @@ -1303,32 +1301,30 @@ class MapModel extends AbstractMapTrackingModel { * @param SystemModel $targetSystem * @return ConnectionModel|null */ - public function searchConnection(SystemModel $sourceSystem, SystemModel $targetSystem){ + public function searchConnection(SystemModel $sourceSystem, SystemModel $targetSystem) : ?ConnectionModel { + $connection = null; + // check if both systems belong to this map if( - $sourceSystem->get('mapId', true) === $this->id && - $targetSystem->get('mapId', true) === $this->id + $sourceSystem->get('mapId', true) === $this->_id && + $targetSystem->get('mapId', true) === $this->_id ){ - $this->filter('connections', [ - 'active = :active AND - ( - ( - source = :sourceId AND - target = :targetId - ) OR ( - source = :targetId AND - target = :sourceId - ) - )', - ':active' => 1, - ':sourceId' => $sourceSystem->id, - ':targetId' => $targetSystem->id, - ], ['limit'=> 1]); + $filter = $this->mergeFilter([ + $this->mergeFilter([self::getFilter('source', $sourceSystem->id, '=', 'A'), self::getFilter('target', $targetSystem->id, '=', 'A')]), + $this->mergeFilter([self::getFilter('source', $targetSystem->id, '=', 'B'), self::getFilter('target', $sourceSystem->id, '=', 'B')]) + ], 'or'); - return ($this->connections) ? reset($this->connections) : null; - }else{ - return null; + $connection = $this->relFindOne('connections', $filter); } + + return $connection; + } + + /** + * @see parent + */ + public function filterRel() : void { + $this->filter('connections', self::getFilter('active', true)); } /** @@ -1447,7 +1443,7 @@ class MapModel extends AbstractMapTrackingModel { * get all maps * @param array $mapIds * @param array $options - * @return \DB\CortexCollection + * @return CortexCollection */ public static function getAll($mapIds = [], $options = []){ $query = [ diff --git a/app/main/model/pathfinder/systemmodel.php b/app/main/model/pathfinder/systemmodel.php index 7fe7b072..118e4538 100644 --- a/app/main/model/pathfinder/systemmodel.php +++ b/app/main/model/pathfinder/systemmodel.php @@ -17,37 +17,37 @@ class SystemModel extends AbstractMapTrackingModel { /** * system position x max */ - const MAX_POS_X = 2440; + const MAX_POS_X = 2440; /** * system position y max */ - const MAX_POS_Y = 1480; + const MAX_POS_Y = 1480; /** * max count of history signature data in cache */ - const MAX_HISTORY_SIGNATURES = 10; + const MAX_SIGNATURES_HISTORY_DATA = 10; /** * TTL for history signature data */ - const TTL_HISTORY_SIGNATURES = 7200; + const TTL_SIGNATURES_HISTORY = 7200; /** * cache key prefix for getData(); result WITH log data */ - const DATA_CACHE_KEY_SIGNATURES = 'SIGNATURES'; + const DATA_CACHE_KEY_SIGNATURES_HISTORY = 'HISTORY_SIGNATURES'; /** * @var string */ - protected $table = 'system'; + protected $table = 'system'; /** * @var array */ - protected $staticSystemDataCache = []; + protected $staticSystemDataCache = []; /** * @var array @@ -258,7 +258,7 @@ class SystemModel extends AbstractMapTrackingModel { /** * get static system data by key * @param string $key - * @return null + * @return mixed|null * @throws \Exception */ private function getStaticSystemValue(string $key){ @@ -483,16 +483,22 @@ class SystemModel extends AbstractMapTrackingModel { public function beforeUpdateEvent($self, $pkeys) : bool { $status = parent::beforeUpdateEvent($self, $pkeys); - if( !$self->isActive()){ + if($status && !$self->isActive()){ // reset "rally point" fields $self->rallyUpdated = 0; $self->rallyPoke = false; // delete connections - $connections = $self->getConnections(); - foreach($connections as $connection){ + foreach($self->getConnections() as $connection){ $connection->erase(); } + + // delete signatures + if(!$self->getMap()->persistentSignatures){ + foreach($self->getSignatures() as $signature){ + $signature->erase(); + } + } } return $status; @@ -538,14 +544,14 @@ class SystemModel extends AbstractMapTrackingModel { * @return logging\LogInterface * @throws \Exception\ConfigException */ - public function newLog($action = '') : Logging\LogInterface{ + public function newLog(string $action = '') : Logging\LogInterface{ return $this->getMap()->newLog($action)->setTempData($this->getLogObjectData()); } /** * @return MapModel */ - public function getMap() : MapModel{ + public function getMap() : MapModel { return $this->get('mapId'); } @@ -562,14 +568,10 @@ class SystemModel extends AbstractMapTrackingModel { * delete a system from a map * hint: signatures and connections will be deleted on cascade * @param CharacterModel $characterModel + * @return bool */ public function delete(CharacterModel $characterModel){ - if( !$this->dry() ){ - // check if character has access - if($this->hasAccess($characterModel)){ - $this->erase(); - } - } + return ($this->valid() && $this->hasAccess($characterModel)) ? $this->erase() : false; } /** @@ -794,8 +796,8 @@ class SystemModel extends AbstractMapTrackingModel { * @param string $stamp * @return array|null */ - public function getSignatureHistoryData(string $stamp) : ?array { - $signatureHistoryData = array_filter($this->getSignaturesHistoryData(), function($historyEntry) use ($stamp){ + public function getSignatureHistoryEntry(string $stamp) : ?array { + $signatureHistoryData = array_filter($this->getSignaturesHistory(), function($historyEntry) use ($stamp){ return md5($historyEntry['stamp']) == $stamp; }); return empty($signatureHistoryData) ? null : reset($signatureHistoryData); @@ -804,22 +806,24 @@ class SystemModel extends AbstractMapTrackingModel { /** * @return array */ - public function getSignaturesHistoryData() : array { - if(!is_array($signaturesHistoryData = $this->getCacheData(self::DATA_CACHE_KEY_SIGNATURES))){ + public function getSignaturesHistory() : array { + if(!is_array($signaturesHistoryData = $this->getCacheData(self::DATA_CACHE_KEY_SIGNATURES_HISTORY))){ $signaturesHistoryData = []; } return $signaturesHistoryData; } /** - * CharacterModel $character + * Updates the signature history cache + * -> each (bulk) change to signatures of this system must result in a new signature history cache entry + * -> This method also clears the cache of this system, so that new signature data gets returned for in getData() * @param CharacterModel $character * @param string $action * @throws \Exception */ - public function updateSignaturesHistory(CharacterModel $character, string $action = 'edit'){ + public function updateSignaturesHistory(CharacterModel $character, string $action = 'edit') : void { if(!$this->dry()){ - $signaturesHistoryData = $this->getSignaturesHistoryData(); + $signaturesHistoryData = $this->getSignaturesHistory(); $historyEntry = [ 'stamp' => microtime(true), 'character' => $character->getBasicData(), @@ -830,9 +834,14 @@ class SystemModel extends AbstractMapTrackingModel { array_unshift($signaturesHistoryData, $historyEntry); // limit max history data - array_splice($signaturesHistoryData, self::MAX_HISTORY_SIGNATURES); + array_splice($signaturesHistoryData, self::MAX_SIGNATURES_HISTORY_DATA); - $this->updateCacheData($signaturesHistoryData, self::DATA_CACHE_KEY_SIGNATURES, self::TTL_HISTORY_SIGNATURES); + $this->updateCacheData($signaturesHistoryData, self::DATA_CACHE_KEY_SIGNATURES_HISTORY, self::TTL_SIGNATURES_HISTORY); + + // clear system cache here + // -> Signature model updates should NOT update the system cache on change + // because a "bulk" change to signatures would clear the system cache multiple times + $this->clearCacheData(); } } diff --git a/app/main/model/pathfinder/systemsignaturemodel.php b/app/main/model/pathfinder/systemsignaturemodel.php index f03fc12e..1c0f9d60 100644 --- a/app/main/model/pathfinder/systemsignaturemodel.php +++ b/app/main/model/pathfinder/systemsignaturemodel.php @@ -173,7 +173,7 @@ class SystemSignatureModel extends AbstractMapTrackingModel { * @return logging\LogInterface * @throws \Exception\ConfigException */ - public function newLog($action = ''): Logging\LogInterface{ + public function newLog(string $action = ''): Logging\LogInterface{ return $this->getMap()->newLog($action)->setTempData($this->getLogObjectData()); } @@ -231,7 +231,7 @@ class SystemSignatureModel extends AbstractMapTrackingModel { * @return bool */ public function delete() : bool { - return !$this->dry() ? $this->erase() : false; + return $this->valid() ? $this->erase() : false; } /** diff --git a/app/main/model/pathfinder/usermodel.php b/app/main/model/pathfinder/usermodel.php index 0ced12f0..f067fe1d 100644 --- a/app/main/model/pathfinder/usermodel.php +++ b/app/main/model/pathfinder/usermodel.php @@ -57,7 +57,7 @@ class UserModel extends AbstractPathfinderModel { * @return \stdClass * @throws Exception */ - public function getData(){ + public function getData() : \stdClass { // get public user data for this user $userData = $this->getSimpleData(); @@ -77,7 +77,7 @@ class UserModel extends AbstractPathfinderModel { // get active character with log data $activeCharacter = $this->getActiveCharacter(); - $userData->character = $activeCharacter->getData(true); + $userData->character = $activeCharacter->getData(true, true); return $userData; } @@ -87,7 +87,7 @@ class UserModel extends AbstractPathfinderModel { * - check out getData() for all user data * @return \stdClass */ - public function getSimpleData(){ + public function getSimpleData() : \stdClass{ $userData = (object) []; $userData->id = $this->id; $userData->name = $this->name; @@ -143,7 +143,7 @@ class UserModel extends AbstractPathfinderModel { * checks whether user has a valid email address and pathfinder has a valid SMTP config * @return bool */ - protected function isMailSendEnabled() : bool{ + protected function isMailSendEnabled() : bool { return Config::isValidSMTPConfig($this->getSMTPConfig()); } @@ -151,7 +151,7 @@ class UserModel extends AbstractPathfinderModel { * get SMTP config for this user * @return \stdClass */ - protected function getSMTPConfig() : \stdClass{ + protected function getSMTPConfig() : \stdClass { $config = Config::getSMTPConfig(); $config->to = $this->email; return $config; @@ -164,7 +164,7 @@ class UserModel extends AbstractPathfinderModel { * @return bool * @throws Exception\ValidationException */ - protected function validate_name(string $key, string $val): bool { + protected function validate_name(string $key, string $val) : bool { $valid = true; if( mb_strlen($val) < 3 || @@ -183,7 +183,7 @@ class UserModel extends AbstractPathfinderModel { * @return bool * @throws Exception\ValidationException */ - protected function validate_email(string $key, string $val): bool { + protected function validate_email(string $key, string $val) : bool { $valid = true; if ( !empty($val) && \Audit::instance()->email($val) == false ){ $valid = false; @@ -196,14 +196,14 @@ class UserModel extends AbstractPathfinderModel { * check whether this character has already a user assigned to it * @return bool */ - public function hasUserCharacters(){ + public function hasUserCharacters() : bool { $this->filter('userCharacters', ['active = ?', 1]); return is_object($this->userCharacters); } /** * get current character data from session - * -> if §characterID == 0 -> get first character data (random) + * -> if $characterId == 0 -> get first character data (random) * @param int $characterId * @param bool $objectCheck * @return array @@ -218,9 +218,12 @@ class UserModel extends AbstractPathfinderModel { // user matches session data if($characterId > 0){ $data = $this->findSessionCharacterData($characterId); - }elseif( !empty($sessionCharacters = (array)$this->getF3()->get(User::SESSION_KEY_CHARACTERS)) ){ + }elseif( + is_array($sessionCharacters = $this->getF3()->get(User::SESSION_KEY_CHARACTERS)) && // check for null + !empty($sessionCharacters) + ){ // no character was requested ($requestedCharacterId = 0) AND session characters were found - // -> get first matched character (e.g. user open browser tab) + // -> get first matched character (e.g. user open /login browser tab) $data = $sessionCharacters[0]; } } @@ -235,7 +238,7 @@ class UserModel extends AbstractPathfinderModel { * @var $character CharacterModel */ $character = AbstractPathfinderModel::getNew('CharacterModel'); - $character->getById( (int)$data['ID']); + $character->getById((int)$data['ID']); if( $character->dry() || @@ -254,7 +257,7 @@ class UserModel extends AbstractPathfinderModel { * @param int $characterId * @return array */ - public function findSessionCharacterData(int $characterId): array { + public function findSessionCharacterData(int $characterId) : array { $data = []; if($characterId){ $sessionCharacters = (array)$this->getF3()->get(User::SESSION_KEY_CHARACTERS); @@ -292,7 +295,7 @@ class UserModel extends AbstractPathfinderModel { * @return null|CharacterModel * @throws Exception */ - public function getActiveCharacter(){ + public function getActiveCharacter() : ?CharacterModel { $activeCharacter = null; $controller = new Controller\Controller(); $currentActiveCharacter = $controller->getCharacter(); @@ -316,7 +319,7 @@ class UserModel extends AbstractPathfinderModel { * get all characters for this user * @return CharacterModel[] */ - public function getCharacters(){ + public function getCharacters() : array { $characters = []; $userCharacters = $this->getUserCharacters(); @@ -339,11 +342,10 @@ class UserModel extends AbstractPathfinderModel { * hint: a user can have multiple active characters * @return CharacterModel[] */ - public function getActiveCharacters(){ + public function getActiveCharacters() : array { $activeCharacters = []; - $userCharacters = $this->getUserCharacters(); - foreach($userCharacters as $userCharacter){ + foreach($this->getUserCharacters() as $userCharacter){ /** * @var $userCharacter UserCharacterModel */ diff --git a/app/pathfinder.ini b/app/pathfinder.ini index d3167fcb..0d7b7416 100644 --- a/app/pathfinder.ini +++ b/app/pathfinder.ini @@ -14,7 +14,7 @@ NAME = Pathfinder ; e.g. public/js/vX.X.X/app.js ; Syntax: String (current version) ; Default: v1.5.0 -VERSION = v1.5.1 +VERSION = v1.5.2 ; Contact information [optional] ; Shown on 'licence', 'contact' page. @@ -255,14 +255,14 @@ DELAY = 5000 ; Requests that exceed the limit are logged as 'warning'. ; Syntax: Integer (milliseconds) ; Default: 200 -EXECUTION_LIMIT = 200 +EXECUTION_LIMIT = 500 [PATHFINDER.TIMER.UPDATE_CLIENT_MAP] ; Execution limit for client side (javascript) map data updates ; Map data updates that exceed the limit are logged as 'warning'. ; Syntax: Integer (milliseconds) ; Default: 50 -EXECUTION_LIMIT = 50 +EXECUTION_LIMIT = 100 [PATHFINDER.TIMER.UPDATE_SERVER_USER_DATA] ; User data update interval (ajax long polling) @@ -275,7 +275,7 @@ DELAY = 5000 ; Requests that exceed the limit are logged as 'warning'. ; Syntax: Integer (milliseconds) ; Default: 500 -EXECUTION_LIMIT = 500 +EXECUTION_LIMIT = 1000 ; update client user data (milliseconds) [PATHFINDER.TIMER.UPDATE_CLIENT_USER_DATA] @@ -283,11 +283,12 @@ EXECUTION_LIMIT = 500 ; User data updates that exceed the limit are logged as 'warning'. ; Syntax: Integer (milliseconds) ; Default: 50 -EXECUTION_LIMIT = 50 +EXECUTION_LIMIT = 100 ; CACHE =========================================================================================== [PATHFINDER.CACHE] -; Delete character log data if nothing (ship/system/...) changed for X seconds +; Checks "character log" data by cronjob after x seconds +; If character is ingame offline -> delete "character log" ; Syntax: Integer (seconds) ; Default: 180 CHARACTER_LOG_INACTIVE = 180 diff --git a/app/requirements.ini b/app/requirements.ini index 81afcba7..35525f68 100644 --- a/app/requirements.ini +++ b/app/requirements.ini @@ -22,9 +22,6 @@ PCRE_VERSION = 8.02 ; Redis extension (optional), required if you want to use Redis as caching Engine (recommended) REDIS = 3.0.0 -; ZeroMQ (ØMQ) extension (optional) required for WebSocket Server extension (recommended) -ZMQ = 1.1.3 - ; Event extension (optional) for WebSocket configuration. Better performance ; https://pecl.php.net/package/event EVENT = 2.3.0 @@ -48,9 +45,6 @@ MAX_INPUT_VARS = 3000 ; Formatted HTML StackTraces HTML_ERRORS = 0 -[REQUIREMENTS.LIBS] -ZMQ = 4.1.3 - [REQUIREMENTS.MYSQL] ; min MySQL Version ; newer "deviation" of MySQL like "MariaDB" > 10.1 are recommended diff --git a/app/routes.ini b/app/routes.ini index fe164ec8..d411c0ec 100644 --- a/app/routes.ini +++ b/app/routes.ini @@ -18,6 +18,9 @@ GET|POST /api/@controller/@action [ajax] = Controller\Api\@cont GET|POST /api/@controller/@action/@arg1 [ajax] = Controller\Api\@controller->@action, 0, 512 GET|POST /api/@controller/@action/@arg1/@arg2 [ajax] = Controller\Api\@controller->@action, 0, 512 +; onUnload route or final map sync (@see https://developer.mozilla.org/docs/Web/API/Navigator/sendBeacon) +POST /api/map/updateUnloadData = Controller\Api\map->updateUnloadData, 0, 512 + [maps] ; REST API wildcard endpoints (not cached, throttled) /api/rest/@controller* [ajax] = Controller\Api\Rest\@controller, 0, 512 diff --git a/composer.lock b/composer.lock new file mode 100644 index 00000000..76ff5629 --- /dev/null +++ b/composer.lock @@ -0,0 +1,2164 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "ad512ee5d1242b7e46b3620b1b05a2a2", + "packages": [ + { + "name": "cache/adapter-common", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-cache/adapter-common.git", + "reference": "6320bb5f5574cb88438059b59f8708da6b6f1d32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-cache/adapter-common/zipball/6320bb5f5574cb88438059b59f8708da6b6f1d32", + "reference": "6320bb5f5574cb88438059b59f8708da6b6f1d32", + "shasum": "" + }, + "require": { + "cache/tag-interop": "^1.0", + "php": "^5.6 || ^7.0", + "psr/cache": "^1.0", + "psr/log": "^1.0", + "psr/simple-cache": "^1.0" + }, + "require-dev": { + "cache/integration-tests": "^0.16", + "phpunit/phpunit": "^5.7.21" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Cache\\Adapter\\Common\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Scherer", + "email": "aequasi@gmail.com", + "homepage": "https://github.com/aequasi" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/nyholm" + } + ], + "description": "Common classes for PSR-6 adapters", + "homepage": "http://www.php-cache.com/en/latest/", + "keywords": [ + "cache", + "psr-6", + "tag" + ], + "time": "2018-07-08T13:04:33+00:00" + }, + { + "name": "cache/array-adapter", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-cache/array-adapter.git", + "reference": "6e9ae7f8bbf1b07bdd6144cb944059f91ae65a14" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-cache/array-adapter/zipball/6e9ae7f8bbf1b07bdd6144cb944059f91ae65a14", + "reference": "6e9ae7f8bbf1b07bdd6144cb944059f91ae65a14", + "shasum": "" + }, + "require": { + "cache/adapter-common": "^1.0", + "cache/hierarchical-cache": "^1.0", + "php": "^5.6 || ^7.0", + "psr/cache": "^1.0", + "psr/simple-cache": "^1.0" + }, + "provide": { + "psr/cache-implementation": "^1.0", + "psr/simple-cache-implementation": "^1.0" + }, + "require-dev": { + "cache/integration-tests": "^0.16", + "phpunit/phpunit": "^5.7.21" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Cache\\Adapter\\PHPArray\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Scherer", + "email": "aequasi@gmail.com", + "homepage": "https://github.com/aequasi" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/nyholm" + } + ], + "description": "A PSR-6 cache implementation using a php array. This implementation supports tags", + "homepage": "http://www.php-cache.com/en/latest/", + "keywords": [ + "array", + "cache", + "psr-6", + "tag" + ], + "time": "2017-11-19T11:08:05+00:00" + }, + { + "name": "cache/filesystem-adapter", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-cache/filesystem-adapter.git", + "reference": "d50680b6dabbe39f9831f5fc9efa61c09d936017" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-cache/filesystem-adapter/zipball/d50680b6dabbe39f9831f5fc9efa61c09d936017", + "reference": "d50680b6dabbe39f9831f5fc9efa61c09d936017", + "shasum": "" + }, + "require": { + "cache/adapter-common": "^1.0", + "league/flysystem": "^1.0", + "php": "^5.6 || ^7.0", + "psr/cache": "^1.0", + "psr/simple-cache": "^1.0" + }, + "provide": { + "psr/cache-implementation": "^1.0" + }, + "require-dev": { + "cache/integration-tests": "^0.16", + "phpunit/phpunit": "^5.7.21" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Cache\\Adapter\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Scherer", + "email": "aequasi@gmail.com", + "homepage": "https://github.com/aequasi" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/nyholm" + } + ], + "description": "A PSR-6 cache implementation using filesystem. This implementation supports tags", + "homepage": "http://www.php-cache.com/en/latest/", + "keywords": [ + "cache", + "filesystem", + "psr-6", + "tag" + ], + "time": "2017-07-16T21:09:25+00:00" + }, + { + "name": "cache/hierarchical-cache", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-cache/hierarchical-cache.git", + "reference": "97173a5115765f72ccdc90dbc01132a3786ef5fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-cache/hierarchical-cache/zipball/97173a5115765f72ccdc90dbc01132a3786ef5fd", + "reference": "97173a5115765f72ccdc90dbc01132a3786ef5fd", + "shasum": "" + }, + "require": { + "cache/adapter-common": "^1.0", + "php": "^5.6 || ^7.0", + "psr/cache": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7.21" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Cache\\Hierarchy\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Scherer", + "email": "aequasi@gmail.com", + "homepage": "https://github.com/aequasi" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/nyholm" + } + ], + "description": "A helper trait and interface to your PSR-6 cache to support hierarchical keys.", + "homepage": "http://www.php-cache.com/en/latest/", + "keywords": [ + "cache", + "hierarchical", + "hierarchy", + "psr-6" + ], + "time": "2017-07-16T21:58:17+00:00" + }, + { + "name": "cache/namespaced-cache", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-cache/namespaced-cache.git", + "reference": "c36d9105c44d6cbbf905b7aafebcb29d488f397c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-cache/namespaced-cache/zipball/c36d9105c44d6cbbf905b7aafebcb29d488f397c", + "reference": "c36d9105c44d6cbbf905b7aafebcb29d488f397c", + "shasum": "" + }, + "require": { + "cache/hierarchical-cache": "^1.0", + "php": "^5.6 || ^7.0", + "psr/cache": "^1.0" + }, + "require-dev": { + "cache/memcached-adapter": "^1.0", + "phpunit/phpunit": "^5.7.21" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Cache\\Namespaced\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/nyholm" + } + ], + "description": "A decorator that makes your cache support namespaces", + "homepage": "http://www.php-cache.com/en/latest/", + "keywords": [ + "cache", + "namespace", + "psr-6" + ], + "time": "2017-07-16T20:20:51+00:00" + }, + { + "name": "cache/redis-adapter", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-cache/redis-adapter.git", + "reference": "95ab6c72739951c6cb44d0051b338bfd5aff806b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-cache/redis-adapter/zipball/95ab6c72739951c6cb44d0051b338bfd5aff806b", + "reference": "95ab6c72739951c6cb44d0051b338bfd5aff806b", + "shasum": "" + }, + "require": { + "cache/adapter-common": "^1.0", + "cache/hierarchical-cache": "^1.0", + "php": "^5.6 || ^7.0", + "psr/cache": "^1.0", + "psr/simple-cache": "^1.0" + }, + "provide": { + "psr/cache-implementation": "^1.0" + }, + "require-dev": { + "cache/integration-tests": "^0.16", + "phpunit/phpunit": "^5.7.21" + }, + "suggest": { + "ext-redis": "The extension required to use this pool." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Cache\\Adapter\\Redis\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Scherer", + "email": "aequasi@gmail.com", + "homepage": "https://github.com/aequasi" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/nyholm" + } + ], + "description": "A PSR-6 cache implementation using Redis (PhpRedis). This implementation supports tags", + "homepage": "http://www.php-cache.com/en/latest/", + "keywords": [ + "cache", + "phpredis", + "psr-6", + "redis", + "tag" + ], + "time": "2017-07-16T21:09:25+00:00" + }, + { + "name": "cache/tag-interop", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-cache/tag-interop.git", + "reference": "c7496dd81530f538af27b4f2713cde97bc292832" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-cache/tag-interop/zipball/c7496dd81530f538af27b4f2713cde97bc292832", + "reference": "c7496dd81530f538af27b4f2713cde97bc292832", + "shasum": "" + }, + "require": { + "php": "^5.5 || ^7.0", + "psr/cache": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Cache\\TagInterop\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/nyholm" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com", + "homepage": "https://github.com/nicolas-grekas" + } + ], + "description": "Framework interoperable interfaces for tags", + "homepage": "http://www.php-cache.com/en/latest/", + "keywords": [ + "cache", + "psr", + "psr6", + "tag" + ], + "time": "2017-03-13T09:14:27+00:00" + }, + { + "name": "cache/void-adapter", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-cache/void-adapter.git", + "reference": "a0554f661cf9f1498a2afe72d077f3c35d797161" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-cache/void-adapter/zipball/a0554f661cf9f1498a2afe72d077f3c35d797161", + "reference": "a0554f661cf9f1498a2afe72d077f3c35d797161", + "shasum": "" + }, + "require": { + "cache/adapter-common": "^1.0", + "cache/hierarchical-cache": "^1.0", + "php": "^5.6 || ^7.0", + "psr/cache": "^1.0", + "psr/simple-cache": "^1.0" + }, + "provide": { + "psr/cache-implementation": "^1.0" + }, + "require-dev": { + "cache/integration-tests": "^0.16", + "phpunit/phpunit": "^5.7.21" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Cache\\Adapter\\Void\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Scherer", + "email": "aequasi@gmail.com", + "homepage": "https://github.com/aequasi" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/nyholm" + } + ], + "description": "A PSR-6 cache implementation using Void. This implementation supports tags", + "homepage": "http://www.php-cache.com/en/latest/", + "keywords": [ + "cache", + "psr-6", + "tag", + "void" + ], + "time": "2017-07-16T21:09:25+00:00" + }, + { + "name": "caseyamcl/guzzle_retry_middleware", + "version": "v2.2", + "source": { + "type": "git", + "url": "https://github.com/caseyamcl/guzzle_retry_middleware.git", + "reference": "6f4b6950d5f6f848c847bd516a9f4ceeb4332287" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/caseyamcl/guzzle_retry_middleware/zipball/6f4b6950d5f6f848c847bd516a9f4ceeb4332287", + "reference": "6f4b6950d5f6f848c847bd516a9f4ceeb4332287", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.3", + "php": "~5.5|~7.0" + }, + "require-dev": { + "nesbot/carbon": "~1.22", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", + "squizlabs/php_codesniffer": "^2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleRetry\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Casey McLaughlin", + "email": "caseyamcl@gmail.com", + "homepage": "https://caseymclaughlin.com", + "role": "Developer" + } + ], + "description": "Guzzle middleware that handles HTTP Retry-After middleware", + "homepage": "https://github.com/caseyamcl/guzzle_retry_middleware", + "keywords": [ + "Guzzle", + "back-off", + "caseyamcl", + "guzzle_retry_middleware", + "middleware", + "retry", + "retry-after" + ], + "time": "2018-06-06T18:20:12+00:00" + }, + { + "name": "clue/ndjson-react", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "c41a30e7f888dc0e4af18881b2c9ab260ba8d6ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/c41a30e7f888dc0e4af18881b2c9ab260ba8d6ce", + "reference": "c41a30e7f888dc0e4af18881b2c9ab260ba8d6ce", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.0 || ^0.7 || ^0.6" + }, + "require-dev": { + "phpunit/phpunit": "^6.0 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\NDJson\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@lueck.tv" + } + ], + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", + "keywords": [ + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" + ], + "time": "2018-05-17T15:31:04+00:00" + }, + { + "name": "doctrine/lexer", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "1febd6c3ef84253d7c815bed85fc622ad207a9f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/1febd6c3ef84253d7c815bed85fc622ad207a9f8", + "reference": "1febd6c3ef84253d7c815bed85fc622ad207a9f8", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "^4.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "time": "2019-06-08T11:03:04+00:00" + }, + { + "name": "egulias/email-validator", + "version": "2.1.9", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "128cc721d771ec2c46ce59698f4ca42b73f71b25" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/128cc721d771ec2c46ce59698f4ca42b73f71b25", + "reference": "128cc721d771ec2c46ce59698f4ca42b73f71b25", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^1.0.1", + "php": ">= 5.5" + }, + "require-dev": { + "dominicsayers/isemail": "dev-master", + "phpunit/phpunit": "^4.8.35||^5.7||^6.0", + "satooshi/php-coveralls": "^1.0.1" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "EmailValidator" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "time": "2019-06-23T10:14:27+00:00" + }, + { + "name": "evenement/evenement", + "version": "v3.0.1", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/531bfb9d15f8aa57454f5f0285b18bec903b8fb7", + "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Evenement": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "time": "2017-07-23T21:35:13+00:00" + }, + { + "name": "exodus4d/pathfinder_esi", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/exodus4d/pathfinder_esi.git", + "reference": "392cb81f1efbd9bf69d7d943f4aefdcf3f0a617c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/exodus4d/pathfinder_esi/zipball/392cb81f1efbd9bf69d7d943f4aefdcf3f0a617c", + "reference": "392cb81f1efbd9bf69d7d943f4aefdcf3f0a617c", + "shasum": "" + }, + "require": { + "cache/void-adapter": "1.0.*", + "caseyamcl/guzzle_retry_middleware": "2.2.*", + "guzzlehttp/guzzle": "6.3.*", + "php-64bit": ">=7.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Exodus4D\\ESI\\": "app/" + } + }, + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Friedrich", + "email": "pathfinder@exodus4d.de" + } + ], + "description": "ESI API library for Pathfinder", + "support": { + "source": "https://github.com/exodus4d/pathfinder_esi/tree/v1.3.1" + }, + "time": "2019-05-07T11:53:44+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba", + "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba", + "shasum": "" + }, + "require": { + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.4", + "php": ">=5.5" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", + "psr/log": "^1.0" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.3-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2018-04-22T15:46:56+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "time": "2016-12-20T10:07:11+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.6.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "239400de7a173fe9901b9ac7c06497751f00727a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a", + "reference": "239400de7a173fe9901b9ac7c06497751f00727a", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" + }, + "suggest": { + "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "time": "2019-07-01T23:21:34+00:00" + }, + { + "name": "league/flysystem", + "version": "1.0.53", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "08e12b7628f035600634a5e76d95b5eb66cea674" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/08e12b7628f035600634a5e76d95b5eb66cea674", + "reference": "08e12b7628f035600634a5e76d95b5eb66cea674", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": ">=5.5.9" + }, + "conflict": { + "league/flysystem-sftp": "<1.0.6" + }, + "require-dev": { + "phpspec/phpspec": "^3.4", + "phpunit/phpunit": "^5.7.10" + }, + "suggest": { + "ext-fileinfo": "Required for MimeType", + "ext-ftp": "Allows you to use FTP server storage", + "ext-openssl": "Allows you to use FTPS server storage", + "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", + "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", + "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", + "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", + "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", + "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", + "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", + "league/flysystem-webdav": "Allows you to use WebDAV storage", + "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", + "spatie/flysystem-dropbox": "Allows you to use Dropbox storage", + "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Filesystem abstraction: Many filesystems, one API.", + "keywords": [ + "Cloud Files", + "WebDAV", + "abstraction", + "aws", + "cloud", + "copy.com", + "dropbox", + "file systems", + "files", + "filesystem", + "filesystems", + "ftp", + "rackspace", + "remote", + "s3", + "sftp", + "storage" + ], + "time": "2019-06-18T20:09:29+00:00" + }, + { + "name": "league/html-to-markdown", + "version": "4.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/html-to-markdown.git", + "reference": "250d1bf45f80d15594fb6b316df777d6d4c97ad1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/250d1bf45f80d15594fb6b316df777d6d4c97ad1", + "reference": "250d1bf45f80d15594fb6b316df777d6d4c97ad1", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xml": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "mikehaertl/php-shellcommand": "~1.1.0", + "phpunit/phpunit": "4.*", + "scrutinizer/ocular": "~1.1" + }, + "bin": [ + "bin/html-to-markdown" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "League\\HTMLToMarkdown\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nick Cernis", + "email": "nick@cern.is", + "homepage": "http://modernnerd.net", + "role": "Original Author" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "An HTML-to-markdown conversion helper for PHP", + "homepage": "https://github.com/thephpleague/html-to-markdown", + "keywords": [ + "html", + "markdown" + ], + "time": "2018-12-24T17:21:44+00:00" + }, + { + "name": "monolog/monolog", + "version": "1.24.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", + "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/log": "~1.0" + }, + "provide": { + "psr/log-implementation": "1.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "graylog2/gelf-php": "~1.0", + "jakub-onderka/php-parallel-lint": "0.9", + "php-amqplib/php-amqplib": "~2.4", + "php-console/php-console": "^3.1.3", + "phpunit/phpunit": "~4.5", + "phpunit/phpunit-mock-objects": "2.3.0", + "ruflin/elastica": ">=0.90 <3.0", + "sentry/sentry": "^0.13", + "swiftmailer/swiftmailer": "^5.3|^6.0" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-mongo": "Allow sending log messages to a MongoDB server", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server", + "sentry/sentry": "Allow sending log messages to a Sentry server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "http://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "time": "2018-11-05T09:00:11+00:00" + }, + { + "name": "psr/cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "time": "2016-08-06T20:24:11+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/log", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2018-11-20T15:27:04+00:00" + }, + { + "name": "psr/simple-cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "time": "2017-10-23T01:57:42+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "react/cache", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "aa10d63a1b40a36a486bdf527f28bac607ee6466" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/aa10d63a1b40a36a486bdf527f28bac607ee6466", + "reference": "aa10d63a1b40a36a486bdf527f28bac607ee6466", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "~2.0|~1.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "time": "2019-07-11T13:45:28+00:00" + }, + { + "name": "react/dns", + "version": "v0.4.19", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "6852fb98e22d2e5bb35fe5aeeaa96551b120e7c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/6852fb98e22d2e5bb35fe5aeeaa96551b120e7c9", + "reference": "6852fb98e22d2e5bb35fe5aeeaa96551b120e7c9", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", + "react/promise": "^2.1 || ^1.2.1", + "react/promise-timer": "^1.2", + "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4.5" + }, + "require-dev": { + "clue/block-react": "^1.2", + "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "time": "2019-07-10T21:00:53+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "a0ecac955c67b57c40fe4a1b88a7cca1b58c982d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/a0ecac955c67b57c40fe4a1b88a7cca1b58c982d", + "reference": "a0ecac955c67b57c40fe4a1b88a7cca1b58c982d", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" + }, + "suggest": { + "ext-event": "~1.0 for ExtEventLoop", + "ext-pcntl": "For signal handling support when using the StreamSelectLoop", + "ext-uv": "* for ExtUvLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "time": "2019-02-07T16:19:49+00:00" + }, + { + "name": "react/promise", + "version": "v2.7.1", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "31ffa96f8d2ed0341a57848cbb84d88b89dd664d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/31ffa96f8d2ed0341a57848cbb84d88b89dd664d", + "reference": "31ffa96f8d2ed0341a57848cbb84d88b89dd664d", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "time": "2019-01-07T21:25:54+00:00" + }, + { + "name": "react/promise-stream", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise-stream.git", + "reference": "00e269d611e9c9a29356aef64c07f7e513e73dc9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise-stream/zipball/00e269d611e9c9a29356aef64c07f7e513e73dc9", + "reference": "00e269d611e9c9a29356aef64c07f7e513e73dc9", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/promise": "^2.1 || ^1.2", + "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4 || ^0.3" + }, + "require-dev": { + "clue/block-react": "^1.0", + "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3", + "react/promise-timer": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Promise\\Stream\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@lueck.tv" + } + ], + "description": "The missing link between Promise-land and Stream-land for ReactPHP", + "homepage": "https://github.com/reactphp/promise-stream", + "keywords": [ + "Buffer", + "async", + "promise", + "reactphp", + "stream", + "unwrap" + ], + "time": "2017-12-22T12:02:05+00:00" + }, + { + "name": "react/promise-timer", + "version": "v1.5.1", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise-timer.git", + "reference": "35fb910604fd86b00023fc5cda477c8074ad0abc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/35fb910604fd86b00023fc5cda477c8074ad0abc", + "reference": "35fb910604fd86b00023fc5cda477c8074ad0abc", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", + "react/promise": "^2.7.0 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Promise\\Timer\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@lueck.tv" + } + ], + "description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.", + "homepage": "https://github.com/reactphp/promise-timer", + "keywords": [ + "async", + "event-loop", + "promise", + "reactphp", + "timeout", + "timer" + ], + "time": "2019-03-27T18:10:32+00:00" + }, + { + "name": "react/socket", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "4d49bd3d6ca0257ada8645dd0f8a2f1885e290b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/4d49bd3d6ca0257ada8645dd0f8a2f1885e290b3", + "reference": "4d49bd3d6ca0257ada8645dd0f8a2f1885e290b3", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^0.4.13", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", + "react/promise": "^2.6.0 || ^1.2.1", + "react/promise-timer": "^1.4.0", + "react/stream": "^1.1" + }, + "require-dev": { + "clue/block-react": "^1.2", + "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "time": "2019-06-03T09:04:16+00:00" + }, + { + "name": "react/stream", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "50426855f7a77ddf43b9266c22320df5bf6c6ce6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/50426855f7a77ddf43b9266c22320df5bf6c6ce6", + "reference": "50426855f7a77ddf43b9266c22320df5bf6c6ce6", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "time": "2019-01-01T16:15:09+00:00" + }, + { + "name": "swiftmailer/swiftmailer", + "version": "v6.2.1", + "source": { + "type": "git", + "url": "https://github.com/swiftmailer/swiftmailer.git", + "reference": "5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a", + "reference": "5397cd05b0a0f7937c47b0adcb4c60e5ab936b6a", + "shasum": "" + }, + "require": { + "egulias/email-validator": "~2.0", + "php": ">=7.0.0", + "symfony/polyfill-iconv": "^1.0", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "require-dev": { + "mockery/mockery": "~0.9.1", + "symfony/phpunit-bridge": "^3.4.19|^4.1.8" + }, + "suggest": { + "ext-intl": "Needed to support internationalized email addresses", + "true/punycode": "Needed to support internationalized email addresses, if ext-intl is not installed" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.2-dev" + } + }, + "autoload": { + "files": [ + "lib/swift_required.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Corbyn" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Swiftmailer, free feature-rich PHP mailer", + "homepage": "https://swiftmailer.symfony.com", + "keywords": [ + "email", + "mail", + "mailer" + ], + "time": "2019-04-21T09:21:45+00:00" + }, + { + "name": "symfony/polyfill-iconv", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-iconv.git", + "reference": "f037ea22acfaee983e271dd9c3b8bb4150bd8ad7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/f037ea22acfaee983e271dd9c3b8bb4150bd8ad7", + "reference": "f037ea22acfaee983e271dd9c3b8bb4150bd8ad7", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-iconv": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.11-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Iconv\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Iconv extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "iconv", + "polyfill", + "portable", + "shim" + ], + "time": "2019-02-06T07:57:58+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "c766e95bec706cdd89903b1eda8afab7d7a6b7af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c766e95bec706cdd89903b1eda8afab7d7a6b7af", + "reference": "c766e95bec706cdd89903b1eda8afab7d7a6b7af", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "symfony/polyfill-mbstring": "^1.3", + "symfony/polyfill-php72": "^1.9" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "time": "2019-03-04T13:44:35+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "fe5e94c604826c35a32fa832f35bd036b6799609" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fe5e94c604826c35a32fa832f35bd036b6799609", + "reference": "fe5e94c604826c35a32fa832f35bd036b6799609", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.11-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2019-02-06T07:57:58+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "ab50dcf166d5f577978419edd37aa2bb8eabce0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/ab50dcf166d5f577978419edd37aa2bb8eabce0c", + "reference": "ab50dcf166d5f577978419edd37aa2bb8eabce0c", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.11-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2019-02-06T07:57:58+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php-64bit": ">=7.1", + "ext-pdo": "*", + "ext-openssl": "*", + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-ctype": "*" + }, + "platform-dev": [] +} diff --git a/js/app.js b/js/app.js index f994e441..ec82a8b4 100644 --- a/js/app.js +++ b/js/app.js @@ -9,14 +9,16 @@ var jsBaseUrl = document.body.getAttribute('data-js-path'); // requireJs configuration requirejs.config({ - baseUrl: 'js', // path for baseUrl - dynamically set !below! ("build_js" | "js") + baseUrl: 'js', // src root path - dynamically set !below! ("build_js" | "js") paths: { - layout: 'layout', - conf: 'app/conf', // path for "config" files dir - dialog: 'app/ui/dialog', // path for "dialog" files dir - templates: '../../templates', // template dir - img: '../../img', // images dir + conf: 'app/conf', // path config files + dialog: 'app/ui/dialog', // path dialog files + layout: 'app/ui/layout', // path layout files + module: 'app/ui/module', // path module files + + templates: '../../templates', // path template base dir + img: '../../img', // path image base dir // main views login: './app/login', // initial start "login page" view @@ -28,24 +30,23 @@ requirejs.config({ jquery: 'lib/jquery-3.3.1.min', // v3.3.1 jQuery bootstrap: 'lib/bootstrap.min', // v3.3.0 Bootstrap js code - http://getbootstrap.com/javascript text: 'lib/requirejs/text', // v2.0.12 A RequireJS/AMD loader plugin for loading text resources. - mustache: 'lib/mustache.min', // v1.0.0 Javascript template engine - http://mustache.github.io - localForage: 'lib/localforage.min', // v1.4.2 localStorage library - https://mozilla.github.io/localForage + mustache: 'lib/mustache.min', // v3.0.1 Javascript template engine - http://mustache.github.io + localForage: 'lib/localforage.min', // v1.7.3 localStorage library - https://localforage.github.io/localForage/ velocity: 'lib/velocity.min', // v1.5.1 animation engine - http://julian.com/research/velocity velocityUI: 'lib/velocity.ui.min', // v5.2.0 plugin for velocity - http://julian.com/research/velocity/#uiPack - slidebars: 'lib/slidebars', // v0.10 Slidebars - side menu plugin http://plugins.adchsm.me/slidebars - jsPlumb: 'lib/dom.jsPlumb-1.7.6', // v1.7.6 jsPlumb (Vanilla)- main map draw plugin https://jsplumbtoolkit.com - farahey: 'lib/farahey-0.5', // v0.5 jsPlumb "magnetizing" extension - https://github.com/jsplumb/farahey + slidebars: 'lib/slidebars', // v2.0.2 Slidebars - side menu plugin https://www.adchsm.com/slidebars/ + jsPlumb: 'lib/jsplumb', // v2.9.3 jsPlumb main map draw plugin http://jsplumb.github.io/jsplumb/home.html + farahey: 'lib/farahey', // v1.1.2 jsPlumb "magnetizing" plugin extension - https://github.com/ThomasChan/farahey customScrollbar: 'lib/jquery.mCustomScrollbar.min', // v3.1.5 Custom scroll bars - http://manos.malihu.gr mousewheel: 'lib/jquery.mousewheel.min', // v3.1.13 Mousewheel - https://github.com/jquery/jquery-mousewheel xEditable: 'lib/bootstrap-editable.min', // v1.5.1 X-editable - in placed editing morris: 'lib/morris.min', // v0.5.1 Morris.js - graphs and charts - raphael: 'lib/raphael-min', // v2.1.2 Raphaël - required for morris (dependency) + raphael: 'lib/raphael.min', // v2.2.8 Raphaël - required for morris - https://dmitrybaranovskiy.github.io/raphael bootbox: 'lib/bootbox.min', // v4.4.0 Bootbox.js - custom dialogs - http://bootboxjs.com easyPieChart: 'lib/jquery.easypiechart.min', // v2.1.6 Easy Pie Chart - HTML 5 pie charts - http://rendro.github.io/easy-pie-chart peityInlineChart: 'lib/jquery.peity.min', // v3.2.1 Inline Chart - http://benpickles.github.io/peity/ dragToSelect: 'lib/jquery.dragToSelect', // v1.1 Drag to Select - http://andreaslagerkvist.com/jquery/drag-to-select hoverIntent: 'lib/jquery.hoverIntent.min', // v1.9.0 Hover intention - http://cherne.net/brian/resources/jquery.hoverIntent.html - fullScreen: 'lib/jquery.fullscreen.min', // v0.6.0 Full screen mode - https://github.com/private-face/jquery.fullscreen select2: 'lib/select2.min', // v4.0.3 Drop Down customization - https://select2.github.io validator: 'lib/validator.min', // v0.10.1 Validator for Bootstrap 3 - https://github.com/1000hz/bootstrap-validator lazylinepainter: 'lib/jquery.lazylinepainter-1.5.1.min', // v1.5.1 SVG line animation plugin - http://lazylinepainter.info @@ -64,13 +65,13 @@ requirejs.config({ easePack: 'lib/EasePack.min', tweenLite: 'lib/TweenLite.min', - // datatables // v1.10.12 DataTables - https://datatables.net + // datatables // v1.10.18 DataTables - https://datatables.net 'datatables.loader': './app/datatables.loader', - 'datatables.net': 'lib/datatables/DataTables-1.10.12/js/jquery.dataTables.min', - 'datatables.net-buttons': 'lib/datatables/Buttons-1.2.1/js/dataTables.buttons.min', - 'datatables.net-buttons-html': 'lib/datatables/Buttons-1.2.1/js/buttons.html5.min', - 'datatables.net-responsive': 'lib/datatables/Responsive-2.1.0/js/dataTables.responsive.min', - 'datatables.net-select': 'lib/datatables/Select-1.2.0/js/dataTables.select.min', + 'datatables.net': 'lib/datatables/DataTables-1.10.18/js/jquery.dataTables.min', + 'datatables.net-buttons': 'lib/datatables/Buttons-1.5.6/js/dataTables.buttons.min', + 'datatables.net-buttons-html': 'lib/datatables/Buttons-1.5.6/js/buttons.html5.min', + 'datatables.net-responsive': 'lib/datatables/Responsive-2.2.2/js/dataTables.responsive.min', + 'datatables.net-select': 'lib/datatables/Select-1.3.0/js/dataTables.select.min', 'datatables.plugins.render.ellipsis': 'lib/datatables/plugins/render/ellipsis', // notification plugin @@ -152,9 +153,6 @@ requirejs.config({ hoverIntent: { deps: ['jquery'] }, - fullScreen: { - deps: ['jquery'] - }, select2: { deps: ['jquery', 'mousewheel'], exports: 'Select2' diff --git a/js/app/admin.js b/js/app/admin.js index 7273c709..64c7198b 100644 --- a/js/app/admin.js +++ b/js/app/admin.js @@ -69,7 +69,7 @@ define([ Util.initDefaultBootboxConfig(); // hide splash loading animation - $('.' + config.splashOverlayClass).hideSplashOverlay(); + $('.' + config.splashOverlayClass + '[data-status="ok"]').hideSplashOverlay(); setPageObserver(); diff --git a/js/app/console.js b/js/app/console.js index 6dadcd90..2b70a1c0 100644 --- a/js/app/console.js +++ b/js/app/console.js @@ -203,9 +203,9 @@ define([], () => { * @param version */ let showVersionInfo = (version) => { - console.ok('%c PATHFINDER', - 'color: #477372; font-size: 25px; margin-left: 10px; line-height: 100px; text-shadow: 1px 1px 0 #212C30; ' + - 'background: url(https://i.imgur.com/1Gw8mjL.png) no-repeat;'); + console.ok('%c PATHFINDER', + 'color: #477372; font-size: 25px; margin-left: 10px; line-height: 50px; text-shadow: 1px 1px 0 #212C30; ' + + 'background: url(https://i.imgur.com/bhSr6LI.png) no-repeat;'); console.pf('Release: %s', version); }; diff --git a/js/app/init.js b/js/app/init.js index 3e3c008e..19fc24a9 100644 --- a/js/app/init.js +++ b/js/app/init.js @@ -2,7 +2,7 @@ * Init */ -define(['jquery'], ($) => { +define([], () => { 'use strict'; @@ -16,7 +16,6 @@ define(['jquery'], ($) => { getCookieCharacterData: '/api/user/getCookieCharacter', // ajax URL - get character data from cookie logIn: '/api/user/logIn', // ajax URL - login logout: '/api/user/logout', // ajax URL - logout - deleteLog: '/api/user/deleteLog', // ajax URL - delete character log openIngameWindow: '/api/user/openIngameWindow', // ajax URL - open inGame Window saveUserConfig: '/api/user/saveAccount', // ajax URL - saves/update user account deleteAccount: '/api/user/deleteAccount', // ajax URL - delete Account data @@ -27,6 +26,7 @@ define(['jquery'], ($) => { getAccessData: '/api/map/getAccessData', // ajax URL - get map access tokens (WebSocket) updateMapData: '/api/map/updateData', // ajax URL - main map update trigger updateUserData: '/api/map/updateUserData', // ajax URL - main map user data trigger + updateUnloadData: '/api/map/updateUnloadData', // post URL - for my sync onUnload // map API saveMap: '/api/map/save', // ajax URL - save/update map deleteMap: '/api/map/delete', // ajax URL - delete map @@ -103,6 +103,14 @@ define(['jquery'], ($) => { class: 'fa-anchor', label: 'anchor', unicode: '' + },{ + class: 'fa-satellite', + label: 'satellite', + unicode: '' + },{ + class: 'fa-skull-crossbones', + label: 'skull crossbones', + unicode: '' },{ class: 'fa-fire', label: 'fire', @@ -119,6 +127,10 @@ define(['jquery'], ($) => { class: 'fa-star', label: 'star', unicode: '' + },{ + class: 'fa-hat-wizard', + label: 'hat wizard', + unicode: '' },{ class: 'fa-plane', label: 'plane', @@ -145,7 +157,6 @@ define(['jquery'], ($) => { unicode: '' } ], - classes: { // log types logTypes: { @@ -343,51 +354,97 @@ define(['jquery'], ($) => { } }, wh_eol: { - cssClass: 'pf-map-connection-wh-eol', - paintStyle: { - dashstyle: '0' // solid line - } + cssClass: 'pf-map-connection-wh-eol' }, wh_fresh: { - cssClass: 'pf-map-connection-wh-fresh', - paintStyle: { - dashstyle: '0' // solid line - } + cssClass: 'pf-map-connection-wh-fresh' }, wh_reduced: { - cssClass: 'pf-map-connection-wh-reduced', - paintStyle: { - dashstyle: '0' // solid line - } + cssClass: 'pf-map-connection-wh-reduced' }, wh_critical: { - cssClass: 'pf-map-connection-wh-critical', - paintStyle: { - dashstyle: '0' // solid line - } + cssClass: 'pf-map-connection-wh-critical' }, - frigate: { - cssClass: 'pf-map-connection-frig', + wh_jump_mass_s: { + cssClass: 'pf-map-connection-wh-size-s', paintStyle: { - dashstyle: '0.99' + dashstyle: '0.5 1', + strokeWidth: 3 }, - overlays:[ + overlays: [ ['Label', { - label: 'frig', - cssClass: ['pf-map-component-overlay', 'frig'].join(' '), - location: 0.6 + label: '', + cssClass: ['pf-map-component-overlay', 'small', 'text-center'].join(' '), + location: 0.65, + id: 'pf-map-connection-jump-mass-overlay' + }] + ] + }, + wh_jump_mass_m: { + cssClass: 'pf-map-connection-wh-size-m', + paintStyle: { + dashstyle: '3 1' + }, + overlays: [ + ['Label', + { + label: '', + cssClass: ['pf-map-component-overlay', 'small', 'text-center'].join(' '), + location: 0.65, + id: 'pf-map-connection-jump-mass-overlay' + }] + ] + }, + wh_jump_mass_l: { + cssClass: 'pf-map-connection-wh-size-l', + overlays: [ + ['Label', + { + label: '', + cssClass: ['pf-map-component-overlay', 'small', 'text-center'].join(' '), + location: 0.65, + id: 'pf-map-connection-jump-mass-overlay' + }] + ] + }, + wh_jump_mass_xl: { + cssClass: 'pf-map-connection-wh-size-xl', + paintStyle: { + strokeWidth: 6 + }, + overlays: [ + ['Label', + { + label: '', + cssClass: ['pf-map-component-overlay', 'small', 'text-center'].join(' '), + location: 0.65, + id: 'pf-map-connection-jump-mass-overlay' }] ] }, preserve_mass: { cssClass: 'pf-map-connection-preserve-mass', - overlays:[ + overlays: [ ['Label', { label: ' save mass', cssClass: ['pf-map-component-overlay', 'mass'].join(' '), - location: 0.6 + location: 0.35 + }] + ] + }, + info_signature: { + overlays: [ + ['Arrow', + { + id: 'pf-map-connection-arrow-overlay', + cssClass: 'pf-map-connection-arrow-overlay', + width: 12, + length: 15, + direction: 1, + foldback: 0.8, + location: 0.5 }] ] }, @@ -396,40 +453,70 @@ define(['jquery'], ($) => { }, state_process: { cssClass: 'pf-map-connection-process', - overlays:[ + overlays: [ ['Label', { label: '', cssClass: ['pf-map-connection-state-overlay'].join(' '), - location: 0.6 + location: 0.5 }] ] } }, + wormholeSizes: { + wh_jump_mass_xl: { + jumpMassMin: 1000000000, + type: 'wh_jump_mass_xl', + class: 'pf-jump-mass-xl', + label: 'XL', + text: 'capital ships' + }, + wh_jump_mass_l: { + jumpMassMin: 300000000, + type: 'wh_jump_mass_l', + class: 'pf-jump-mass-l', + label: 'L', + text: 'larger ships' + }, + wh_jump_mass_m: { + jumpMassMin: 20000000, + type: 'wh_jump_mass_m', + class: 'pf-jump-mass-m', + label: 'M', + text: 'medium ships' + }, + wh_jump_mass_s: { + jumpMassMin: 1000, + type: 'wh_jump_mass_s', + class: 'pf-jump-mass-s', + label: 'S', + text: 'smallest ships' + } + }, // signature groups signatureGroups: { 1: { - name: '(combat site|kampfgebiet|site de combat|Боевой район)', //* + name: '(combat site|kampfgebiet|site de combat|Боевой район|战斗地点)', label: 'Combat' }, 2: { - name: '(relic site|reliktgebiet|site de reliques|Археологический район)', //* + name: '(relic site|reliktgebiet|site de reliques|Археологический район|遗迹地点)', label: 'Relic' }, 3: { - name: '(data site|datengebiet|site de données|Информационный район)', + name: '(data site|datengebiet|site de données|Информационный район|数据地点)', label: 'Data' }, 4: { - name: '(gas site|gasgebiet|site de collecte de gaz|Газовый район)', + name: '(gas site|gasgebiet|site de collecte de gaz|Газовый район|气云地点)', label: 'Gas' }, 5: { - name: '(wormhole|wurmloch|trou de ver|Червоточина)', + name: '(wormhole|wurmloch|trou de ver|Червоточина|虫洞)', label: 'Wormhole' }, 6: { - name: '(ore site|mineraliengebiet|site de minerai|Астероидный район)', + name: '(ore site|mineraliengebiet|site de minerai|Астероидный район|矿石地点)', label: 'Ore' }, 7: { diff --git a/js/app/key.js b/js/app/key.js index bab7d1e8..4d4d5d6f 100644 --- a/js/app/key.js +++ b/js/app/key.js @@ -10,6 +10,12 @@ define([ label: 'Close open dialog', keyNames: ['ESC'] }, + // map ---------------------------------------------------------------------------------------------- + mapMove: { + group: 'map', + label: 'Move map section', + keyNames: ['space', 'drag'] + }, // signature ---------------------------------------------------------------------------------------- signatureSelect: { group: 'signatures', @@ -37,6 +43,11 @@ define([ keyNames: ['CONTROL', 'V'], alias: 'paste' }, + renameSystem: { + group: 'map', + label: 'Rename system', + keyNames: ['ALT', 'N'] + }, newSignature: { group: 'signatures', label: 'New Signature', @@ -79,12 +90,6 @@ define([ */ let debug = false; - /** - * check interval for "new" active keys - * @type {number} - */ - let keyWatchPeriod = 100; - /** * DOM data key for an element that lists all active events (comma separated) * @type {string} @@ -133,6 +138,13 @@ define([ return Object.keys(map); }; + /** + * checks whether a key is currently active (keydown) + * @param key + * @returns {boolean} + */ + let isActive = key => map.hasOwnProperty(key) && map[key] === true; + /** * callback function that compares two arrays * @param element @@ -460,6 +472,7 @@ define([ }; return { + isActive: isActive, getGroupedShortcuts: getGroupedShortcuts }; }); \ No newline at end of file diff --git a/js/app/logging.js b/js/app/logging.js index aee42183..5b36851d 100644 --- a/js/app/logging.js +++ b/js/app/logging.js @@ -7,7 +7,7 @@ define([ 'app/init', 'app/util', 'bootbox' -], function($, Init, Util, bootbox){ +], ($, Init, Util, bootbox) => { 'use strict'; @@ -27,27 +27,16 @@ define([ moduleHeadlineIconClass: 'pf-module-icon-button' // class for toolbar icons in the head }; - /** - * get log time string - * @returns {string} - */ - let getLogTime = function(){ - let serverTime = Util.getServerTime(); - let logTime = serverTime.toLocaleTimeString('en-US', { hour12: false }); - - return logTime; - }; - /** * updated "sync status" dynamic dialog area */ - let updateSyncStatus = function(){ + let updateSyncStatus = () => { // check if task manager dialog is open let logDialog = $('#' + config.taskDialogId); if(logDialog.length){ // dialog is open - requirejs(['text!templates/modules/sync_status.html', 'mustache'], function(templateSyncStatus, Mustache){ + requirejs(['text!templates/modules/sync_status.html', 'mustache'], (templateSyncStatus, Mustache) => { let data = { timestampCounterClass: config.timestampCounterClass, syncStatus: Init.syncStatus, @@ -59,7 +48,7 @@ define([ } }; - let syncStatusElement = $( Mustache.render(templateSyncStatus, data ) ); + let syncStatusElement = $(Mustache.render(templateSyncStatus, data )); logDialog.find('.' + config.taskDialogStatusAreaClass).html( syncStatusElement ); @@ -76,7 +65,7 @@ define([ /** * shows the logging dialog */ - let showDialog = function(){ + let showDialog = () => { // dialog content requirejs(['text!templates/dialog/task_manager.html', 'mustache', 'datatables.loader'], function(templateTaskManagerDialog, Mustache){ @@ -108,19 +97,27 @@ define([ buttons: [ { extend: 'copy', + tag: 'a', className: config.moduleHeadlineIconClass, - text: ' copy' + text: ' copy', + exportOptions: { + orthogonal: 'export' + } }, { extend: 'csv', + tag: 'a', className: config.moduleHeadlineIconClass, - text: ' csv' + text: ' csv', + exportOptions: { + orthogonal: 'export' + } } ] }, paging: true, ordering: true, - order: [ 1, 'desc' ], + order: [1, 'desc'], hover: false, pageLength: 10, lengthMenu: [[5, 10, 25, 50, 100, -1], [5, 10, 25, 50, 100, 'All']], @@ -134,42 +131,69 @@ define([ columnDefs: [ { targets: 0, + name: 'status', title: '', - width: '18px', + width: 18, searchable: false, class: ['text-center'].join(' '), - data: 'status' + data: 'status', + render: { + display: (cellData, type, rowData, meta) => { + let statusClass = Util.getLogInfo(cellData, 'class'); + return ''; + } + } },{ targets: 1, - title: '  ', - width: '50px', + name: 'time', + title: '', + width: 50, searchable: true, class: 'text-right', - data: 'time' + data: 'timestamp', + render: { + display: (cellData, type, rowData, meta) => rowData.timestampFormatted + } },{ targets: 2, - title: '  ', - width: '35px', + name: 'duration', + title: '', + width: 35, searchable: false, class: 'text-right', - sType: 'html', - data: 'duration' + data: 'duration', + render: { + display: (cellData, type, rowData, meta) => { + let logStatus = getLogStatusByDuration(rowData.key, cellData); + let statusClass = Util.getLogInfo(logStatus, 'class'); + return '' + cellData + 'ms'; + } + } },{ targets: 3, + name: 'description', title: 'description', searchable: true, data: 'description' },{ targets: 4, + name: 'logType', title: 'type', - width: '40px', + width: 40, searchable: true, class: ['text-center'].join(' '), - data: 'type' + data: 'logType', + render: { + display: (cellData, type, rowData, meta) => { + let typeIconClass = getLogTypeIconClass(cellData); + return ''; + } + } },{ targets: 5, - title: 'Prozess-ID   ', - width: '80px', + name: 'process', + title: 'Prozess-ID', + width: 80, searchable: false, class: 'text-right', data: 'key' @@ -197,10 +221,7 @@ define([ // show Morris graphs ---------------------------------------------------------- - // function for chart label formation - let labelYFormat = function(y){ - return Math.round(y) + 'ms'; - }; + let labelYFormat = val => Math.round(val) + 'ms'; for(let key in chartData){ if(chartData.hasOwnProperty(key)){ @@ -241,8 +262,8 @@ define([ }); headline.append(averageElement); - colElementGraph.append( headline ); - colElementGraph.append( graphArea ); + colElementGraph.append(headline); + colElementGraph.append(graphArea); graphArea.showLoadingAnimation(); @@ -321,10 +342,10 @@ define([ * @param key * @param duration (if undefined -> just update graph with current data) */ - let updateLogGraph = function(key, duration){ + let updateLogGraph = (key, duration) => { // check if graph data already exist - if( !(chartData.hasOwnProperty(key))){ + if(!(chartData.hasOwnProperty(key))){ chartData[key] = {}; chartData[key].data = []; chartData[key].graph = null; @@ -341,7 +362,7 @@ define([ chartData[key].data = chartData[key].data.slice(0, maxGraphDataCount); } - function getGraphData(data){ + let getGraphData = data => { let tempChartData = { data: [], dataSum: 0, @@ -352,7 +373,7 @@ define([ let value = 0; if(data[x]){ value = data[x]; - tempChartData.dataSum = Number( (tempChartData.dataSum + value).toFixed(2) ); + tempChartData.dataSum = Number((tempChartData.dataSum + value).toFixed(2)); } tempChartData.data.push({ @@ -362,10 +383,10 @@ define([ } // calculate average - tempChartData.average = Number( ( tempChartData.dataSum / data.length ).toFixed(2) ); + tempChartData.average = Number((tempChartData.dataSum / data.length).toFixed(2)); return tempChartData; - } + }; let tempChartData = getGraphData(chartData[key].data); @@ -374,17 +395,17 @@ define([ let avgElement = chartData[key].averageElement; let updateElement = chartData[key].updateElement; - let delay = Util.getCurrentTriggerDelay( key, 0 ); + let delay = Util.getCurrentTriggerDelay(key, 0); if(delay){ - updateElement[0].textContent = ' delay: ' + delay + 'ms '; + updateElement[0].textContent = ' delay: ' + delay.toFixed(2) + ' ms'; } // set/change average line chartData[key].graph.options.goals = [tempChartData.average]; // change avg. display - avgElement[0].textContent = 'Avg. ' + tempChartData.average + 'ms'; + avgElement[0].textContent = 'avg. ' + tempChartData.average.toFixed(2) + ' ms'; let avgStatus = getLogStatusByDuration(key, tempChartData.average); let avgStatusClass = Util.getLogInfo( avgStatus, 'class'); @@ -417,9 +438,9 @@ define([ * @param logDuration * @returns {string} */ - let getLogStatusByDuration = function(logKey, logDuration){ + let getLogStatusByDuration = (logKey, logDuration) => { let logStatus = 'info'; - if( logDuration > Init.timer[logKey].EXECUTION_LIMIT ){ + if(logDuration > Init.timer[logKey].EXECUTION_LIMIT){ logStatus = 'warning'; } return logStatus; @@ -430,8 +451,7 @@ define([ * @param logType * @returns {string} */ - let getLogTypeIconClass = function(logType){ - + let getLogTypeIconClass = logType => { let logIconClass = ''; switch(logType){ @@ -470,24 +490,23 @@ define([ let logDuration = options.duration; let logType = options.type; - // check log status by duration - let logStatus = getLogStatusByDuration(logKey, logDuration); - let statusClass = Util.getLogInfo( logStatus, 'class'); - let typeIconClass = getLogTypeIconClass(logType); - // update graph data updateLogGraph(logKey, logDuration); + let time = Util.getServerTime(); + let timestamp = time.getTime(); + let timestampFormatted = time.toLocaleTimeString('en-US', { hour12: false }); + let logRowData = { - status: '', - time: getLogTime(), - duration: '' + logDuration + 'ms', + status: getLogStatusByDuration(logKey, logDuration), + timestamp: timestamp, + timestampFormatted: timestampFormatted, + duration: logDuration, description: logDescription, - type: '', + logType: logType, key: logKey }; - if(logDataTable){ // add row if dataTable is initialized before new log logDataTable.row.add( logRowData ).draw(false); @@ -500,7 +519,7 @@ define([ // delete old log entries from table --------------------------------- let rowCount = logData.length; - if( rowCount >= maxEntries ){ + if(rowCount >= maxEntries){ if(logDataTable){ logDataTable.rows(0, {order:'index'}).remove().draw(false); @@ -520,7 +539,6 @@ define([ return { init: init, - getLogTime: getLogTime, showDialog: showDialog }; }); \ No newline at end of file diff --git a/js/app/login.js b/js/app/login.js index c544f308..14525389 100644 --- a/js/app/login.js +++ b/js/app/login.js @@ -10,9 +10,9 @@ define([ 'blueImpGallery', 'bootbox', 'lazyload', - 'app/ui/header', - 'app/ui/logo', - 'app/ui/demo_map', + 'layout/header_login', + 'layout/logo', + 'layout/demo_map', 'dialog/account_settings', 'dialog/notification', 'dialog/manual', @@ -798,7 +798,7 @@ define([ }); // hide splash loading animation - $('.' + config.splashOverlayClass).hideSplashOverlay(); + $('.' + config.splashOverlayClass + '[data-status="ok"]').hideSplashOverlay(); // init server status information initServerStatus(); diff --git a/js/app/map/contextmenu.js b/js/app/map/contextmenu.js index 26fc68f2..4fd522ee 100644 --- a/js/app/map/contextmenu.js +++ b/js/app/map/contextmenu.js @@ -93,20 +93,25 @@ define([ let moduleData = { id: config.connectionContextMenuId, items: [ - {icon: 'fa-plane', action: 'frigate', text: 'frigate hole'}, + {icon: 'fa-hourglass-end', action: 'wh_eol', text: 'toggle EOL'}, {icon: 'fa-exclamation-triangle', action: 'preserve_mass', text: 'preserve mass'}, - {icon: 'fa-crosshairs', action: 'change_scope', text: 'change scope', subitems: [ - {subIcon: 'fa-minus-circle', subIconClass: '', subAction: 'scope_wh', subText: 'wormhole'}, - {subIcon: 'fa-minus-circle', subIconClass: 'txt-color txt-color-indigoDarkest', subAction: 'scope_stargate', subText: 'stargate'}, - {subIcon: 'fa-minus-circle', subIconClass: 'txt-color txt-color-tealLighter', subAction: 'scope_jumpbridge', subText: 'jumpbridge'} + {icon: 'fa-reply fa-rotate-180', action: 'change_status', text: 'mass status', subitems: [ + {subIcon: 'fa-circle', subIconClass: 'txt-color txt-color-gray', subAction: 'status_fresh', subText: 'stage 1 (fresh)'}, + {subIcon: 'fa-circle', subIconClass: 'txt-color txt-color-orange', subAction: 'status_reduced', subText: 'stage 2 (reduced)'}, + {subIcon: 'fa-circle', subIconClass: 'txt-color txt-color-redDarker', subAction: 'status_critical', subText: 'stage 3 (critical)'} ]}, - {icon: 'fa-reply fa-rotate-180', action: 'change_status', text: 'change status', subitems: [ - {subIcon: 'fa-clock', subAction: 'wh_eol', subText: 'toggle EOL'}, - {subDivider: true}, - {subIcon: 'fa-circle', subAction: 'status_fresh', subText: 'stage 1 (fresh)'}, - {subIcon: 'fa-adjust', subAction: 'status_reduced', subText: 'stage 2 (reduced)'}, - {subIcon: 'fa-circle', subAction: 'status_critical', subText: 'stage 3 (critical)'} + {icon: 'fa-reply fa-rotate-180', action: 'wh_jump_mass_change', text: 'ship size', subitems: [ + {subIcon: 'fa-char', subChar: 'S', subAction: 'wh_jump_mass_s', subText: 'smallest ships'}, + {subIcon: 'fa-char', subChar: 'M', subAction: 'wh_jump_mass_m', subText: 'medium ships'}, + {subIcon: 'fa-char', subChar: 'L', subAction: 'wh_jump_mass_l', subText: 'larger ships'}, + {subIcon: 'fa-char', subChar: 'XL', subAction: 'wh_jump_mass_xl', subText: 'capital ships'} + + ]}, + {icon: 'fa-crosshairs', action: 'change_scope', text: 'change scope', subitems: [ + {subIcon: 'fa-minus-circle', subIconClass: '', subAction: 'scope_wh', subText: 'wormhole'}, + {subIcon: 'fa-minus-circle', subIconClass: 'txt-color txt-color-indigoDarkest', subAction: 'scope_stargate', subText: 'stargate'}, + {subIcon: 'fa-minus-circle', subIconClass: 'txt-color txt-color-tealLighter', subAction: 'scope_jumpbridge', subText: 'jumpbridge'} ]}, {divider: true, action: 'separator'} , diff --git a/js/app/map/layout.js b/js/app/map/layout.js index a40f95e5..fcf1f051 100644 --- a/js/app/map/layout.js +++ b/js/app/map/layout.js @@ -38,7 +38,7 @@ define(() => { * @returns {*} * @private */ - this._getElementDimension = (element) => { + this._getElementDimension = element => { let dim = null; let left = 0; diff --git a/js/app/map/magnetizing.js b/js/app/map/magnetizing.js index f63f8ed2..48239a22 100644 --- a/js/app/map/magnetizing.js +++ b/js/app/map/magnetizing.js @@ -1,135 +1,173 @@ /** * Map "magnetizing" feature - * jsPlumb extension used: http://morrisonpitt.com/farahey/ + * jsPlumb extension used: https://github.com/ThomasChan/farahey */ define([ 'jquery', 'app/map/util', 'farahey' -], function($, MapUtil){ +], ($, MapUtil) => { 'use strict'; /** - * Cached current "Magnetizer" object - * @type {Magnetizer} + * active magnetizer instances (cache object) + * @type {{}} */ - let m8 = null; + let magnetizerInstances = {}; /** - * init a jsPlumb (map) Element for "magnetised" function. - * this is optional and prevents systems from being overlapped + * magnetizer instance exists for mapId + * @param mapId + * @returns {boolean} */ - $.fn.initMagnetizer = function(){ - let mapContainer = this; - let systems = mapContainer.getSystems(); + let hasInstance = mapId => magnetizerInstances.hasOwnProperty(mapId); - /** - * helper function - * get current system offset - * @param system - * @returns {{left, top}} - * @private - */ - let _offset = function(system){ + /** + * get magnetizer instance by mapId + * @param mapId + * @returns {null} + */ + let getInstance = mapId => hasInstance(mapId) ? magnetizerInstances[mapId] : null; - let _ = function(p){ - let v = system.style[p]; - return parseInt(v.substring(0, v.length - 2)); - }; - - return { - left:_('left'), - top:_('top') - }; - }; - - /** - * helper function - * set new system offset - * @param system - * @param o - * @private - */ - let _setOffset = function(system, o){ - let markAsUpdated = false; - - // new position must be within parent container - // no negative offset! - if( - o.left >= 0 && - o.left <= 2300 - ){ - markAsUpdated = true; - system.style.left = o.left + 'px'; - } - - if( - o.top >= 0 && - o.top <= 498 - ){ - markAsUpdated = true; - system.style.top = o.top + 'px'; - } - - if(markAsUpdated === true){ - MapUtil.markAsChanged($(system)); - } - }; - - /** - * helper function - * exclude current dragged element(s) from position update - * @param id - * @returns {boolean} - * @private - */ - let _dragFilter = function(id){ - return !$('#' + id).is('.jsPlumb_dragged, .pf-system-locked'); - }; - - let gridConstrain = function(gridX, gridY){ - return function(id, current, delta){ - if( mapContainer.hasClass(MapUtil.config.mapGridClass) ){ - // active grid - return { - left:(gridX * Math.floor( (current[0] + delta.left) / gridX )) - current[0], - top:(gridY * Math.floor( (current[1] + delta.top) / gridY )) - current[1] - }; - }else{ - // no grid - return delta; - } - }; - }; - - // main init for "magnetize" feature ------------------------------------------------------ - m8 = new Magnetizer({ - container: mapContainer, - getContainerPosition: function(c){ - return c.offset(); - }, - getPosition:_offset, - getSize: function(system){ - return [ $(system).outerWidth(), $(system).outerHeight() ]; - }, - getId : function(system){ - return $(system).attr('id'); - }, - setPosition:_setOffset, - elements: systems, - filter: _dragFilter, - padding: [6, 6], - constrain: gridConstrain(MapUtil.config.mapSnapToGridDimension, MapUtil.config.mapSnapToGridDimension) - }); + /** + * set new magnetizer instance for mapId + * @param mapId + * @param magnetizer + */ + let setInstance = (mapId, magnetizer) => { + if(mapId && magnetizer){ + magnetizerInstances[mapId] = magnetizer; + } }; - $.fn.destroyMagnetizer = function(){ - let mapContainer = this; + /** + * init new magnetizer instance for a map + * @param mapContainer + */ + let initMagnetizer = mapContainer => { + let mapId = mapContainer.data('id'); - // remove cached "magnetizer" instance - m8 = null; + if(!hasInstance(mapId)){ + // magnetizer not exist -> new instance + let systems = mapContainer.getSystems(); + + /** + * function that takes an element from your list and returns its position as a JS object + * @param system + * @returns {{top: number, left: number}} + * @private + */ + let _offset = system => { + let _ = p => { + let v = system.style[p]; + return parseInt(v.substring(0, v.length - 2)); + }; + return {left: _('left'), top: _('top')}; + }; + + /** + * function that takes an element id and position, and applies that position to the related element + * @param system + * @param o + * @private + */ + let _setOffset = (system, o) => { + o.left = Math.round(o.left); + o.top = Math.round(o.top); + let left = o.left + 'px'; + let top = o.top + 'px'; + let markAsUpdated = false; + + // new position must be within parent container + // no negative offset! + if( + o.left >= 0 && o.left <= 2300 && + system.style.left !== left + ){ + system.style.left = left; + markAsUpdated = true; + } + + if( + o.top >= 0 && o.top <= 1400 && + system.style.top !== top + ){ + system.style.top = top; + markAsUpdated = true; + } + + if(markAsUpdated){ + MapUtil.markAsChanged($(system)); + } + }; + + /** + * filter some element8s) from being moved + * @param systemId + * @returns {boolean} + * @private + */ + let _dragFilter = systemId => { + let filterClasses = ['jtk-drag', 'pf-system-locked']; + return ![...document.getElementById(systemId).classList].some(className => filterClasses.indexOf(className) >= 0); + }; + + /** + * grid snap constraint + * @param gridX + * @param gridY + * @returns {Function} + */ + let gridConstrain = (gridX, gridY) => { + return (id, current, delta) => { + if(mapContainer.hasClass(MapUtil.config.mapGridClass)){ + // active grid + return { + left: (gridX * Math.floor( (Math.round(current[0]) + delta.left) / gridX )) - current[0], + top: (gridY * Math.floor( (Math.round(current[1]) + delta.top) / gridY )) - current[1] + }; + }else{ + // no grid + delta.left = Math.round(delta.left); + delta.top = Math.round(delta.top); + return delta; + } + }; + }; + + // create new magnetizer instance ------------------------------------------------------------------------- + setInstance(mapId, window.Farahey.getInstance({ + container: mapContainer, + getContainerPosition: mapContainer => mapContainer.offset(), + getPosition:_offset, + getSize: system => { + let clientRect = system.getBoundingClientRect(); + return [Math.floor(clientRect.width), Math.floor(clientRect.height)]; + }, + getId : system => system.id, + setPosition:_setOffset, + elements: systems.toArray(), + filter: _dragFilter, + padding: [3, 3], + constrain: gridConstrain(MapUtil.config.mapSnapToGridDimension, MapUtil.config.mapSnapToGridDimension), + executeNow: false, // no initial rearrange after initialization + excludeFocus: true + })); + } + }; + + /** + * destroy Magnetizer instance + */ + let destroyMagnetizer = mapContainer => { + let mapId = mapContainer.data('id'); + let magnetizer = getInstance(mapId); + if(magnetizer){ + magnetizer.reset(); + delete magnetizerInstances[mapId]; + } }; /** @@ -137,44 +175,50 @@ define([ * @param map * @param e */ - let executeAtEvent = function(map, e){ - if(m8 !== null && e ){ - m8.executeAtEvent(e); + let executeAtEvent = (map, e) => { + let mapContainer = $(map.getContainer()); + let mapId = mapContainer.data('id'); + let magnetizer = getInstance(mapId); + + if(magnetizer && e){ + magnetizer.executeAtEvent(e, { + iterations: 2, + excludeFocus: true + }); map.repaintEverything(); } }; /** - * rearrange all systems of a map - * needs "magnetization" to be active - * @param map + * add system to magnetizer instance + * @param mapId + * @param system + * @param doNotTestForDuplicates */ - let executeAtCenter = function(map){ - if(m8 !== null){ - m8.executeAtCenter(); - map.repaintEverything(); + let addElement = (mapId, system, doNotTestForDuplicates) => { + let magnetizer = getInstance(mapId); + if(magnetizer){ + magnetizer.addElement(system, doNotTestForDuplicates); } }; /** - * set/update elements for "magnetization" - * -> (e.g. new systems was added) - * @param map + * remove system element from magnetizer instance + * @param mapId + * @param system */ - let setElements = function(map){ - if(m8 !== null){ - let mapContainer = $(map.getContainer()); - let systems = mapContainer.getSystems(); - m8.setElements(systems); - - // re-arrange systems - executeAtCenter(map); + let removeElement = (mapId, system) => { + let magnetizer = getInstance(mapId); + if(magnetizer){ + magnetizer.removeElement(system); } }; return { - executeAtCenter: executeAtCenter, + initMagnetizer: initMagnetizer, + destroyMagnetizer: destroyMagnetizer, executeAtEvent: executeAtEvent, - setElements: setElements + addElement: addElement, + removeElement: removeElement }; }); \ No newline at end of file diff --git a/js/app/map/map.js b/js/app/map/map.js index a79bf830..8da672a6 100644 --- a/js/app/map/map.js +++ b/js/app/map/map.js @@ -6,17 +6,19 @@ define([ 'jquery', 'app/init', 'app/util', + 'app/key', 'bootbox', 'app/map/util', 'app/map/contextmenu', + 'app/map/overlay/overlay', + 'app/map/overlay/util', 'app/map/system', 'app/map/layout', 'app/map/magnetizing', 'app/map/scrollbar', 'dragToSelect', - 'app/map/overlay', 'app/map/local' -], ($, Init, Util, bootbox, MapUtil, MapContextMenu, System, Layout, MagnetizerWrapper) => { +], ($, Init, Util, Key, bootbox, MapUtil, MapContextMenu, MapOverlay, MapOverlayUtil, System, Layout, Magnetizer, Scrollbar) => { 'use strict'; @@ -60,6 +62,33 @@ define([ // -> those maps queue their updates until "pf:unlocked" event let mapUpdateQueue = []; + + // map menu options + let mapOptions = { + mapMagnetizer: { + buttonId: Util.config.menuButtonMagnetizerId, + description: 'Magnetizer', + onEnable: Magnetizer.initMagnetizer, + onDisable: Magnetizer.destroyMagnetizer + }, + mapSnapToGrid : { + buttonId: Util.config.menuButtonGridId, + description: 'Grid snapping', + class: 'mapGridClass' + }, + mapSignatureOverlays : { + buttonId: Util.config.menuButtonEndpointId, + description: 'Endpoint overlay', + onEnable: MapOverlay.showInfoSignatureOverlays, + onDisable: MapOverlay.hideInfoSignatureOverlays, + }, + mapCompact : { + buttonId: Util.config.menuButtonCompactId, + description: 'Compact system layout', + class: 'mapCompactClass' + } + }; + /** * checks mouse events on system head elements * -> prevents drag/drop system AND drag/drop connections on some child elements @@ -91,8 +120,7 @@ define([ }, 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' + // isSource:true }, target: { filter: filterSystemHeadEvent, @@ -104,9 +132,7 @@ define([ hoverClass: config.systemActiveClass, activeClass: 'dragActive' }, - // isTarget:true, - // uniqueEndpoint: false, - anchor: 'Continuous' + // uniqueEndpoint: false }, endpointTypes: Init.endpointTypes, connectionTypes: Init.connectionTypes @@ -130,10 +156,10 @@ define([ // -> 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('pf-map-endpoint-overlay'); + let overlay = endpoint.getOverlay(MapOverlayUtil.config.endpointOverlayId); if(overlay instanceof jsPlumb.Overlays.Label){ - let label = overlay.getParameter('label'); - overlay.setLocation(MapUtil.getLabelEndpointOverlayLocation(endpoint, label)); + let labels = overlay.getParameter('signatureLabels'); + overlay.setLocation(MapUtil.getEndpointOverlaySignatureLocation(endpoint, labels)); } } } @@ -143,13 +169,13 @@ define([ /** * updates a system with current information * @param map + * @param system * @param data * @param currentUserIsHere boolean - if the current user is in this system * @param options */ - $.fn.updateSystemUserData = function(map, data, currentUserIsHere, options){ - let system = $(this); - let systemId = system.attr('id'); + let updateSystemUserData = (map, system, data, currentUserIsHere = false, options = {}) => { + let systemIdAttr = system.attr('id'); let compactView = Util.getObjVal(options, 'compactView'); // find countElement -> minimizedUI @@ -161,16 +187,21 @@ define([ // 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 oldCacheKey = system.data('userCacheKey'); + let oldUserCount = system.data('userCount') || 0; + let userWasHere = Boolean(system.data('currentUser')); let userCounter = 0; - system.data('currentUser', false); + system.data('currentUser', currentUserIsHere); - // if current user is in THIS system trigger event - if(currentUserIsHere){ - system.data('currentUser', true); + // auto select system if current user is in THIS system + if( + currentUserIsHere && + !userWasHere && + Boolean(Util.getObjVal(Init, 'character.autoLocationSelect')) && + Boolean(Util.getObjVal(Util.getCurrentUserData(), 'character.selectLocation')) + ){ + Util.triggerMenuAction(map.getContainer(), 'SelectSystem', {systemId: system.data('id'), forceSelect: false}); } // add user information @@ -178,24 +209,28 @@ define([ data && data.user ){ + userCounter = data.user.length; + + // loop all active pilots and build cache-key let cacheArray = []; + for(let tempUserData of data.user){ + cacheArray.push(tempUserData.id + '_' + tempUserData.log.ship.id); + } + + // make sure cacheArray values are sorted for key comparison + let collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'}); + cacheArray.sort(collator.compare); // we need to add "view mode" option to key // -> if view mode change detected -> key no longer valid - cacheArray.push(compactView ? 'compact' : 'default'); + cacheArray.unshift(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('_'); + let cacheKey = cacheArray.join('_').hashCode(); // check for if cacheKey has changed if(cacheKey !== oldCacheKey){ // set new CacheKey - system.data('userCache', cacheKey); + system.data('userCacheKey', cacheKey); system.data('userCount', userCounter); // remove all content @@ -209,7 +244,7 @@ define([ systemHeadExpand.hide(); system.toggleBody(false, map, {}); - map.revalidate(systemId); + map.revalidate(systemIdAttr); }else{ systemCount.empty(); @@ -251,7 +286,7 @@ define([ } let tooltipOptions = { - systemId: systemId, + systemId: systemIdAttr, highlight: highlight, userCount: userCounter }; @@ -264,14 +299,14 @@ define([ complete: function(system){ // show active user tooltip system.toggleSystemTooltip('show', tooltipOptions); - map.revalidate( systemId ); + map.revalidate(systemIdAttr); } }); } } }else{ // no user data found for this system - system.data('userCache', false); + system.data('userCacheKey', false); system.data('userCount', 0); systemBody.empty(); @@ -285,7 +320,7 @@ define([ systemHeadExpand.hide(); system.toggleBody(false, map, {}); - map.revalidate(systemId); + map.revalidate(systemIdAttr); } } }; @@ -588,7 +623,7 @@ define([ case 'change_status_empty': case 'change_status_unscanned': // change system status - system.getMapOverlay('timer').startMapUpdateCounter(); + MapOverlayUtil.getMapOverlay(system, 'timer').startMapUpdateCounter(); let statusString = action.split('_'); @@ -676,7 +711,7 @@ define([ } } - // save filterScopes in IndexDB + // store filterScopes in IndexDB MapUtil.storeLocalData('map', mapId, 'filterScopes', filterScopes); MapUtil.filterMapByScopes(map, filterScopes); @@ -690,11 +725,11 @@ define([ break; case 'map_edit': // open map edit dialog tab - $(document).triggerMenuEvent('ShowMapSettings', {tab: 'edit'}); + Util.triggerMenuAction(document, 'ShowMapSettings', {tab: 'edit'}); break; case 'map_info': // open map info dialog tab - $(document).triggerMenuEvent('ShowMapInfo', {tab: 'information'}); + Util.triggerMenuAction(document, 'ShowMapInfo', {tab: 'information'}); break; } }; @@ -719,28 +754,32 @@ define([ switch(action){ case 'delete_connection': // delete a single connection - - // confirm dialog - bootbox.confirm('Is this connection really gone?', function(result){ + bootbox.confirm('Is this connection really gone?', 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); + 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]; - mapElement.getMapOverlay('timer').startMapUpdateCounter(); - - MapUtil.setConnectionWHStatus(connection, 'wh_' + newStatus); + 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': @@ -749,12 +788,12 @@ define([ let newScope = action.split('_')[1]; let newScopeName = MapUtil.getScopeInfoForConnection( newScope, 'label'); - bootbox.confirm('Change scope from ' + scopeName + ' to ' + newScopeName + '?', function(result){ + bootbox.confirm('Change scope from ' + scopeName + ' to ' + newScopeName + '?', result => { if(result){ - mapElement.getMapOverlay('timer').startMapUpdateCounter(); - setConnectionScope(connection, newScope); + MapOverlayUtil.getMapOverlay(mapElement, 'timer').startMapUpdateCounter(); + Util.showNotify({title: 'Connection scope changed', text: 'New scope: ' + newScopeName, type: 'success'}); MapUtil.markAsChanged(connection); @@ -775,7 +814,7 @@ define([ switch(action){ case 'bubble': - mapElement.getMapOverlay('timer').startMapUpdateCounter(); + MapOverlayUtil.getMapOverlay(mapElement, 'timer').startMapUpdateCounter(); endpoint.toggleType(action); for(let connection of endpoint.connections){ @@ -824,15 +863,25 @@ define([ // connector has changed connection.setConnector(newConnector); - // remove all connection types - connection.clearTypes(); + 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)); - // set new new connection type - // if scope changed -> connection type == scope - connection.setType(MapUtil.getDefaultConnectionTypeByScope(scope)); + // 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 + ); } }; @@ -860,7 +909,7 @@ define([ source: sourceSystem[0], target: targetSystem[0], scope: connectionData.scope || map.Defaults.Scope, - type: (connectionData.type || MapUtil.getDefaultConnectionTypeByScope(map.Defaults.Scope)).join(' ') + //type: (connectionData.type || MapUtil.getDefaultConnectionTypeByScope(map.Defaults.Scope)).join(' ') /* experimental set "static" connection parameters in initial load parameters: { connectionId: connectionId, @@ -878,6 +927,7 @@ define([ // 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,..) @@ -931,15 +981,25 @@ define([ * @returns {*} */ let updateConnection = (connection, newConnectionData) => { - let connectionData = MapUtil.getDataByConnection(connection); + // 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 connectionData - // -> if "process" type exists, add it types for removal + // type "process" is not included in currentConnectionData ---------------------------------------------------- + // -> if "process" type exists, remove it if(connection.hasType('state_process')){ - connectionData.type.push('state_process'); + MapUtil.removeConnectionTypes(connection, ['state_process']); } // check id, IDs should never change but must be set after initial save --------------------------------------- @@ -948,40 +1008,57 @@ define([ } // update scope ----------------------------------------------------------------------------------------------- - if(connectionData.scope !== newConnectionData.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); } - 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){ + if(currentConnectionData.source !== newConnectionData.source){ map.setSource(connection, MapUtil.getSystemId(mapId, newConnectionData.source)); } - if(connectionData.target !== newConnectionData.target){ + if(currentConnectionData.target !== newConnectionData.target){ map.setTarget(connection, MapUtil.getSystemId(mapId, newConnectionData.target)); } - // update connection types ------------------------------------------------------------------------------------ - let checkAvailability = (arr, val) => arr.some(arrVal => arrVal === val); + // update connection types ==================================================================================== - 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); - } + // 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); } - for(let type of removeType){ - if(checkAvailability(['wh_eol', 'frigate', 'preserve_mass', 'state_process'], type)){ - connection.removeType(type); - } + // 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); } - // update endpoints ------------------------------------------------------------------------------------------- + // 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 @@ -1004,7 +1081,7 @@ define([ } } - // set update date (important for update check) + // set update date (important for update check) =============================================================== // important: set parameters ONE-by-ONE! // -> (setParameters() will overwrite all previous params) connection.setParameter('created', newConnectionData.created); @@ -1145,7 +1222,7 @@ define([ mapWrapper.setMapShortcuts(); // show static overlay actions - let mapOverlay = mapContainer.getMapOverlay('info'); + let mapOverlay = MapOverlayUtil.getMapOverlay(mapContainer, 'info'); mapOverlay.updateOverlayIcon('systemRegion', 'show'); mapOverlay.updateOverlayIcon('connection', 'show'); mapOverlay.updateOverlayIcon('connectionEol', 'show'); @@ -1179,7 +1256,6 @@ define([ 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){ @@ -1220,9 +1296,8 @@ define([ } } - if( addNewSystem === true){ + if(addNewSystem === true){ drawSystem(mapConfig.map, systemData); - newSystems++; } } @@ -1316,7 +1391,7 @@ define([ deleteConnection.source && deleteConnection.target ){ - mapConfig.map.detach(deleteConnection, {fireEvent: false}); + mapConfig.map.deleteConnection(deleteConnection, {fireEvent: false}); } } } @@ -1325,11 +1400,6 @@ define([ // 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 ){ @@ -1346,10 +1416,13 @@ define([ }); }; - return new Promise(updateMapExecutor).then(payload => { - - let filterMapByScopesExecutor = (resolve, reject) => { - // apply current active scope filter ================================================================== + /** + * apply current active scope filter + * @param payload + * @returns {Promise} + */ + let filterMapByScopes = payload => { + let filterMapByScopesExecutor = resolve => { let promiseStore = MapUtil.getLocaleData('map', payload.data.mapConfig.config.id); promiseStore.then(dataStore => { let scopes = []; @@ -1363,7 +1436,31 @@ define([ }; return new Promise(filterMapByScopesExecutor); - }); + }; + + /** + * show signature overlays + * @param payload + * @returns {Promise} + */ + let showInfoSignatureOverlays = payload => { + let showInfoSignatureOverlaysExecutor = resolve => { + let promiseStore = MapUtil.getLocaleData('map', payload.data.mapConfig.config.id); + promiseStore.then(dataStore => { + if(dataStore && dataStore.mapSignatureOverlays){ + MapOverlay.showInfoSignatureOverlays($(payload.data.mapConfig.map.getContainer())); + } + + resolve(payload); + }); + }; + + return new Promise(showInfoSignatureOverlaysExecutor); + }; + + return new Promise(updateMapExecutor) + .then(showInfoSignatureOverlays) + .then(filterMapByScopes); }; /** @@ -1406,23 +1503,14 @@ define([ }; /** - * get a connection object from "cache" (this requires the "connectionCache" cache to be actual! + * get a connection object from "cache" + * -> this requires the "connectionCache" cache is up2date! * @param mapId * @param connectionId - * @returns {*} + * @returns {*|null} */ $.fn.getConnectionById = function(mapId, connectionId){ - - let connection = null; - - if( - connectionCache[mapId] && - connectionCache[mapId][connectionId] - ){ - connection = connectionCache[mapId][connectionId]; - } - - return connection; + return Util.getObjVal(connectionCache, [mapId, connectionId].join('.')) || null; }; /** @@ -1505,6 +1593,9 @@ define([ // set system observer setSystemObserver(map, newSystem); + // register system to "magnetizer" + Magnetizer.addElement(systemData.mapId, newSystem[0]); + // connect new system (if connection data is given) if(connectedSystem){ @@ -1626,12 +1717,12 @@ define([ Util.showNotify({title: title, text: 'Scope: ' + scope, type: 'success'}); }else{ // some save errors - payload.context.map.detach(payload.context.connection, {fireEvent: false}); + payload.context.map.deleteConnection(payload.context.connection, {fireEvent: false}); } }, payload => { // remove this connection from map - payload.context.map.detach(payload.context.connection, {fireEvent: false}); + payload.context.map.deleteConnection(payload.context.connection, {fireEvent: false}); Util.handleAjaxErrorResponse(payload); } ); @@ -1737,22 +1828,26 @@ define([ // hidden menu actions if(scope === 'abyssal'){ - options.hidden.push('frigate'); + 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('frigate'); + 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('frigate'); + 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'); @@ -1762,13 +1857,14 @@ define([ 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'); } + 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){ @@ -1778,6 +1874,11 @@ define([ options.active.push('status_fresh'); } + // disabled menu actions + if(connection.getParameter('sizeLocked')){ + options.disabled.push('wh_jump_mass_change'); + } + resolve(options); }; @@ -1830,7 +1931,7 @@ define([ start: function(params){ let dragSystem = $(params.el); - mapOverlayTimer = dragSystem.getMapOverlay('timer'); + mapOverlayTimer = MapOverlayUtil.getMapOverlay(dragSystem, 'timer'); // start map update timer mapOverlayTimer.startMapUpdateCounter(); @@ -1865,7 +1966,7 @@ define([ // 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); + Magnetizer.executeAtEvent(map, p.e); }, stop: function(params){ let dragSystem = $(params.el); @@ -1968,11 +2069,7 @@ define([ * @param sourceSystem */ let saveSystemCallback = (map, newSystemData, sourceSystem) => { - // draw new system to map drawSystem(map, newSystemData, sourceSystem); - - // re/arrange systems (prevent overlapping) - MagnetizerWrapper.setElements(map); }; /** @@ -2047,7 +2144,7 @@ define([ revalidate(map, system); if(!hideCounter){ - $(system).getMapOverlay('timer').startMapUpdateCounter(); + MapOverlayUtil.getMapOverlay(system, 'timer').startMapUpdateCounter(); } }; @@ -2063,18 +2160,33 @@ define([ 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 + 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: { - 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. + 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 }, - 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 + 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 }); @@ -2127,13 +2239,13 @@ define([ // set "default" connection status only for NEW connections if(!connection.suspendedElement){ - MapUtil.setConnectionWHStatus(connection, MapUtil.getDefaultConnectionTypeByScope(connection.scope)); + 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?', function(result){ + 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); @@ -2181,6 +2293,13 @@ define([ }); + // 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){ @@ -2200,6 +2319,18 @@ define([ }); + // Notification the current zoom was changed + newJsPlumbInstance.bind('zoom', function(zoom){ + MapOverlay.updateZoomOverlay(this); + + // store new zoom level in IndexDB + if(zoom === 1){ + MapUtil.deleteLocalData('map', mapId, 'mapZoom'); + }else{ + MapUtil.storeLocalData('map', mapId, 'mapZoom', zoom); + } + }); + // ======================================================================================================== // Events for interactive CSS classes https://community.jsplumbtoolkit.com/doc/styling-via-css.html //========================================================================================================= @@ -2276,6 +2407,8 @@ define([ // get map container let mapContainer = $(map.getContainer()); + MapOverlay.initMapDebugOverlays(map); + // context menu for mapContainer mapContainer.on('contextmenu', function(e){ e.preventDefault(); @@ -2329,7 +2462,7 @@ define([ } }, onShow: function(){ - $(document).trigger('pf:closeMenu', [{}]); + Util.triggerMenuAction(document, 'Close'); }, onRefresh: function(){ } @@ -2449,112 +2582,116 @@ define([ // 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){ - 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 - }); + 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){ - $(this).destroyPopover(); + 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.systemHeadInfoClass + ' span[class^="pf-system-sec-"]' + selector: '.' + config.systemClass }); // catch events =============================================================================================== - // toggle global map option (e.g. "grid snap", "magnetization") - mapContainer.on('pf:menuMapOption', function(e, mapOption){ - let mapElement = $(this); - + /** + * update/toggle global map option (e.g. "grid snap", "magnetization") + * @param mapContainer + * @param data + */ + let updateMapOption = (mapContainer, data) => { // get map menu config options - let data = MapUtil.mapOptions[mapOption.option]; + let mapOption = mapOptions[data.option]; - let promiseStore = MapUtil.getLocaleData('map', mapElement.data('id')); + let promiseStore = MapUtil.getLocaleData('map', mapContainer.data('id')); promiseStore.then(function(dataStore){ let notificationText = 'disabled'; - let button = $('#' + this.data.buttonId); + let button = $('#' + this.mapOption.buttonId); let dataExists = false; if( dataStore && - dataStore[this.mapOption.option] + dataStore[this.data.option] ){ dataExists = true; } - if(dataExists === mapOption.toggle){ + if(dataExists === this.data.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] ); + if(this.mapOption.class){ + this.mapContainer.removeClass(MapUtil.config[this.mapOption.class]); } - // call optional jQuery extension on mapElement - if(this.data.onDisable){ - $.fn[ this.data.onDisable ].apply(this.mapElement); + // call optional jQuery extension on mapContainer + if(this.mapOption.onDisable && !this.data.skipOnDisable){ + this.mapOption.onDisable(this.mapContainer); } // show map overlay info icon - this.mapElement.getMapOverlay('info').updateOverlayIcon(this.mapOption.option, 'hide'); + MapOverlayUtil.getMapOverlay(this.mapContainer, 'info').updateOverlayIcon(this.data.option, 'hide'); // delete map option - MapUtil.deleteLocalData('map', this.mapElement.data('id'), this.mapOption.option ); + MapUtil.deleteLocalData('map', this.mapContainer.data('id'), this.data.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] ); + if(this.mapOption.class){ + this.mapContainer.addClass(MapUtil.config[this.mapOption.class]); } - // call optional jQuery extension on mapElement - if(this.data.onEnable){ - $.fn[ this.data.onEnable ].apply(this.mapElement); + // call optional jQuery extension on mapContainer + if(this.mapOption.onEnable && !this.data.skipOnEnable){ + this.mapOption.onEnable(this.mapContainer); } // hide map overlay info icon - this.mapElement.getMapOverlay('info').updateOverlayIcon(this.mapOption.option, 'show'); + MapOverlayUtil.getMapOverlay(this.mapContainer, 'info').updateOverlayIcon(this.data.option, 'show'); // store map option - MapUtil.storeLocalData('map', this.mapElement.data('id'), this.mapOption.option, 1 ); + MapUtil.storeLocalData('map', this.mapContainer.data('id'), this.data.option, 1); notificationText = 'enabled'; } - if(mapOption.toggle){ - Util.showNotify({title: this.data.description, text: notificationText, type: 'info'}); + if(this.data.toggle){ + Util.showNotify({title: this.mapOption.description, text: notificationText, type: 'info'}); } }.bind({ - mapOption: mapOption, data: data, - mapElement: mapElement + mapOption: mapOption, + mapContainer: mapContainer })); - }); + }; - // 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); + /** + * 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 ... @@ -2571,12 +2708,45 @@ define([ } if(select){ - let mapWrapper = mapElement.closest('.' + config.mapWrapperClass); - mapWrapper.scrollToSystem(MapUtil.getSystemPosition(system)); + let mapWrapper = mapContainer.closest('.' + config.mapWrapperClass); + Scrollbar.scrollToSystem(mapWrapper, MapUtil.getSystemPosition(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, 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 @@ -2603,7 +2773,7 @@ define([ // update "local" overlay for this map mapContainer.on('pf:updateLocal', function(e, userData){ let mapElement = $(this); - let mapOverlay = mapElement.getMapOverlay('local'); + let mapOverlay = MapOverlayUtil.getMapOverlay(mapElement, 'local'); if(userData && userData.config && userData.config.id){ let currentMapData = Util.getCurrentMapData(userData.config.id); @@ -2704,36 +2874,21 @@ define([ let compactView = mapElement.hasClass(MapUtil.config.mapCompactClass); // get current character log data - let characterLogExists = false; - let currentCharacterLog = Util.getCurrentCharacterLog(); + let characterLogSystemId = Util.getObjVal(Util.getCurrentCharacterLog(), '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, // inactive users (no location) - currentLocation: { - id: 0, // systemId for current active user - name: false // systemName for current active user - } + userCountInactive: 0 }; - 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){ + for(let system of mapElement.find('.' + config.systemClass)){ system = $(system); let systemId = system.data('systemId'); let tempUserData = null; @@ -2761,20 +2916,15 @@ define([ // the current user can only be in a single system ------------------------------------------------ if( - characterLogExists && - currentCharacterLog.system.id === systemId + !currentUserOnMap && + characterLogSystemId && + characterLogSystemId === 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; - } + currentUserIsHere = true; + currentUserOnMap = true; } - system.updateSystemUserData(map, tempUserData, currentUserIsHere, {compactView: compactView}); + updateSystemUserData(map, system, tempUserData, currentUserIsHere, {compactView: compactView}); } // users who are not in any map system ---------------------------------------------------------------- @@ -2807,7 +2957,7 @@ define([ 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()){ + if(!MapOverlayUtil.getMapOverlayInterval(mapContainer)){ mapData = mapContainer.getMapDataFromClient(filter, minimal); } return mapData; @@ -2962,7 +3112,8 @@ define([ let mapElement = $(mapConfig.map.getContainer()); MapUtil.setMapDefaultOptions(mapElement, mapConfig.config) .then(payload => MapUtil.visualizeMap(mapElement, 'show')) - .then(payload => MapUtil.scrollToDefaultPosition(mapElement)) + .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'}); }) @@ -2992,7 +3143,7 @@ define([ */ let loadMapExecutor = (resolve, reject) => { // init jsPlumb - jsPlumb.ready(function(){ + jsPlumb.ready(() => { // get new map instance or load existing mapConfig.map = getMapInstance(mapConfig.config.id); @@ -3025,6 +3176,96 @@ define([ mapWrapperElement.initCustomScrollbar({ callbacks: { + onInit: function(){ + // 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 @@ -3039,7 +3280,7 @@ define([ }, onScrollStart: function(){ // hide all open xEditable fields - $(this).find('.editable').editable('hide'); + $(this).find('.editable.editable-open').editable('hide'); // hide all system head tooltips $(this).find('.' + config.systemHeadClass + ' .fa').tooltip('hide'); @@ -3051,7 +3292,6 @@ define([ // add map overlays after scrollbar is initialized // because of its absolute position mapWrapperElement.initMapOverlays(); - mapWrapperElement.initLocalOverlay(mapId); }; diff --git a/js/app/map/overlay.js b/js/app/map/overlay.js deleted file mode 100644 index d7318c9d..00000000 --- a/js/app/map/overlay.js +++ /dev/null @@ -1,747 +0,0 @@ -/** - * map overlay functions - */ - -define([ - 'jquery', - 'app/init', - 'app/util', - 'app/map/util' -], ($, Init, Util, MapUtil) => { - 'use strict'; - - let config = { - logTimerCount: 3, // map log timer in seconds - - // map - mapWrapperClass: 'pf-map-wrapper', // wrapper div (scrollable) - - // map overlay positions - mapOverlayClass: 'pf-map-overlay', // class for all map overlays - mapOverlayTimerClass: 'pf-map-overlay-timer', // class for map overlay timer e.g. map timer - mapOverlayInfoClass: 'pf-map-overlay-info', // class for map overlay info e.g. map info - overlayLocalClass: 'pf-map-overlay-local', // class for map overlay "local" table - - // system - systemHeadClass: 'pf-system-head', // class for system head - - // overlay IDs - connectionOverlayWhId: 'pf-map-connection-wh-overlay', // connection WH overlay ID (jsPlumb) - connectionOverlayEolId: 'pf-map-connection-eol-overlay', // connection EOL overlay ID (jsPlumb) - connectionOverlayArrowId: 'pf-map-connection-arrow-overlay', // connection Arrows overlay ID (jsPlumb) - - endpointOverlayId: 'pf-map-endpoint-overlay', // endpoint overlay ID (jsPlumb) - - // overlay classes - componentOverlayClass: 'pf-map-component-overlay', // class for "normal size" overlay - - connectionArrowOverlayClass: 'pf-map-connection-arrow-overlay', // class for "connection arrow" overlay - connectionDiamondOverlayClass: 'pf-map-connection-diamond-overlay' // class for "connection diamond" overlay - }; - - /** - * get MapObject (jsPlumb) from mapElement - * @param mapElement - * @returns {*} - */ - let getMapObjectFromMapElement = mapElement => { - let Map = require('app/map/map'); - return Map.getMapInstance( mapElement.data('id') ); - }; - - /** - * get map object (jsPlumb) from iconElement - * @param overlayIcon - * @returns {*} - */ - let getMapObjectFromOverlayIcon = overlayIcon => { - let mapElement = Util.getMapElementFromOverlay(overlayIcon); - - return getMapObjectFromMapElement( mapElement ); - }; - - /** - * add overlays to connections (signature based data) - * @param connections - * @param connectionsData - */ - let addConnectionsOverlay = (connections, connectionsData) => { - let SystemSignatures = require('app/ui/module/system_signature'); - - /** - * add label to endpoint - * @param endpoint - * @param label - */ - let addEndpointOverlay = (endpoint, label) => { - label = label.join(', '); - - endpoint.addOverlay([ - 'Label', - { - label: MapUtil.getEndpointOverlayContent(label), - id: config.endpointOverlayId, - cssClass: [config.componentOverlayClass, label.length ? 'small' : 'icon'].join(' '), - location: MapUtil.getLabelEndpointOverlayLocation(endpoint, label), - parameters: { - label: label - } - } - ]); - }; - - // loop through all map connections (get from DOM) - for(let connection of connections){ - let connectionId = connection.getParameter('connectionId'); - let sourceEndpoint = connection.endpoints[0]; - let targetEndpoint = connection.endpoints[1]; - - let signatureTypeNames = { - sourceLabels: [], - targetLabels: [] - }; - - // ... find matching connectionData (from Ajax) - for(let connectionData of connectionsData){ - if(connectionData.id === connectionId){ - signatureTypeNames = MapUtil.getConnectionDataFromSignatures(connection, connectionData); - // ... connection matched -> continue with next one - break; - } - } - - let sourceLabel = signatureTypeNames.sourceLabels; - let targetLabel = signatureTypeNames.targetLabels; - - // add endpoint overlays ------------------------------------------------------ - addEndpointOverlay(sourceEndpoint, sourceLabel); - addEndpointOverlay(targetEndpoint, targetLabel); - - // add arrow (connection) overlay that points from "XXX" => "K162" ------------ - let overlayType = 'Diamond'; // not specified - let arrowDirection = 1; - - if( - (sourceLabel.indexOf('K162') !== -1 && targetLabel.indexOf('K162') !== -1) || - (sourceLabel.length === 0 && targetLabel.length === 0) || - ( - sourceLabel.length > 0 && targetLabel.length > 0 && - sourceLabel.indexOf('K162') === -1 && targetLabel.indexOf('K162') === -1 - ) - ){ - // unknown direction - overlayType = 'Diamond'; // not specified - arrowDirection = 1; - }else if( - (sourceLabel.indexOf('K162') !== -1) || - (sourceLabel.length === 0 && targetLabel.indexOf('K162') === -1) - ){ - // convert default arrow direction - overlayType = 'Arrow'; - arrowDirection = -1; - }else{ - // default arrow direction is fine - overlayType = 'Arrow'; - arrowDirection = 1; - } - - connection.addOverlay([ - overlayType, - { - width: 12, - length: 15, - location: 0.5, - foldback: 0.85, - direction: arrowDirection, - id: config.connectionOverlayArrowId, - cssClass: (overlayType === 'Arrow') ? config.connectionArrowOverlayClass : config.connectionDiamondOverlayClass - } - ]); - - } - }; - - /** - * remove overviews from a Tooltip - * @param endpoint - * @param i - */ - let removeEndpointOverlay = (endpoint, i) => { - endpoint.removeOverlays(config.endpointOverlayId); - }; - - /** - * format json object with "time parts" into string - * @param parts - * @returns {string} - */ - let formatTimeParts = parts => { - let label = ''; - if(parts.days){ - label += parts.days + 'd '; - } - label += ('00' + parts.hours).slice(-2); - label += ':' + ('00' + parts.min).slice(-2); - return label; - }; - - /** - * hide default icon and replace it with "loading" icon - * @param iconElement - */ - let showLoading = iconElement => { - iconElement = $(iconElement); - let dataName = 'default-icon'; - let defaultIconClass = iconElement.data(dataName); - - // get default icon class - if( !defaultIconClass ){ - // index 0 == 'fa-fw', index 1 == IconName - defaultIconClass = $(iconElement).attr('class').match(/\bfa-\S*/g)[1]; - iconElement.data(dataName, defaultIconClass); - } - - iconElement.toggleClass( defaultIconClass + ' fa-sync fa-spin'); - }; - - /** - * hide "loading" icon and replace with default icon - * @param iconElement - */ - let hideLoading = iconElement => { - iconElement = $(iconElement); - let dataName = 'default-icon'; - let defaultIconClass = iconElement.data(dataName); - - iconElement.toggleClass( defaultIconClass + ' fa-sync fa-spin'); - }; - - /** - * git signature data that is linked to a connection for a mapId - * @param mapElement - * @param connections - * @param callback - */ - let getConnectionSignatureData = (mapElement, connections, callback) => { - let mapOverlay = $(mapElement).getMapOverlay('info'); - let overlayConnectionIcon = mapOverlay.find('.pf-map-overlay-endpoint'); - - showLoading(overlayConnectionIcon); - - let requestData = { - mapId: mapElement.data('id'), - addData : ['signatures'], - filterData : ['signatures'] - }; - - $.ajax({ - type: 'POST', - url: Init.path.getMapConnectionData, - data: requestData, - dataType: 'json', - context: { - mapElement: mapElement, - connections: connections, - overlayConnectionIcon: overlayConnectionIcon - } - }).done(function(connectionsData){ - // hide all connection before add them (refresh) - this.mapElement.hideEndpointOverlays(); - // ... add overlays - callback(this.connections, connectionsData); - }).always(function(){ - hideLoading(this.overlayConnectionIcon); - }); - }; - - /** - * showEndpointOverlays - * -> used by "refresh" overlays (hover) AND/OR initial menu trigger - */ - $.fn.showEndpointOverlays = function(){ - let mapElement = $(this); - let map = getMapObjectFromMapElement(mapElement); - let MapUtil = require('app/map/util'); - let connections = MapUtil.searchConnectionsByScopeAndType(map, 'wh', undefined, true); - - // get connection signature information --------------------------------------- - getConnectionSignatureData(mapElement, connections, addConnectionsOverlay); - }; - - /** - * hideEndpointOverlays - * -> see showEndpointOverlays() - */ - $.fn.hideEndpointOverlays = function(){ - let map = getMapObjectFromMapElement($(this)); - let MapUtil = require('app/map/util'); - let connections = MapUtil.searchConnectionsByScopeAndType(map, 'wh'); - - for(let connection of connections){ - connection.removeOverlays(config.connectionOverlayArrowId); - connection.endpoints.forEach(removeEndpointOverlay); - } - }; - - /** - * Overlay options (all available map options shown in overlay) - * "active": (active || hover) indicated whether an icon/option - * is marked as "active". - * "active": Makes icon active when visible - * "hover": Make icon active on hover - */ - let options = { - filter: { - title: 'active filter', - trigger: 'active', - class: 'pf-map-overlay-filter', - iconClass: ['fas', 'fa-fw', 'fa-filter'], - onClick: function(e){ - // clear all filter - let mapElement = Util.getMapElementFromOverlay(this); - let map = getMapObjectFromOverlayIcon(this); - - MapUtil.storeLocalData('map', mapElement.data('id'), 'filterScopes', []); - MapUtil.filterMapByScopes(map, []); - } - }, - mapSnapToGrid: { - title: 'active grid', - trigger: 'active', - class: 'pf-map-overlay-grid', - iconClass: ['fas', 'fa-fw', 'fa-th'] - }, - mapMagnetizer: { - title: 'active magnetizer', - trigger: 'active', - class: 'pf-map-overlay-magnetizer', - iconClass: ['fas', 'fa-fw', 'fa-magnet'] - }, - systemRegion: { - title: 'show regions', - trigger: 'hover', - class: 'pf-map-overlay-region', - iconClass: ['fas', 'fa-fw', 'fa-tags'], - hoverIntent: { - over: function(e){ - let mapElement = Util.getMapElementFromOverlay(this); - mapElement.find('.' + config.systemHeadClass).each(function(){ - let systemHead = $(this); - // init popover if not already exists - if(!systemHead.data('bs.popover')){ - let system = systemHead.parent(); - systemHead.popover({ - placement: 'right', - html: true, - trigger: 'manual', - container: mapElement, - title: false, - content: Util.getSystemRegionTable( - system.data('region'), - system.data('faction') || null - ) - }); - } - systemHead.setPopoverSmall(); - systemHead.popover('show'); - }); - }, - out: function(e){ - let mapElement = Util.getMapElementFromOverlay(this); - mapElement.find('.' + config.systemHeadClass).popover('hide'); - } - } - }, - mapEndpoint: { - title: 'refresh signature overlays', - trigger: 'refresh', - class: 'pf-map-overlay-endpoint', - iconClass: ['fas', 'fa-fw', 'fa-link'], - hoverIntent: { - over: function(e){ - let mapElement = Util.getMapElementFromOverlay(this); - mapElement.showEndpointOverlays(); - }, - out: function(e){ - // just "refresh" on hover - } - } - }, - mapCompact: { - title: 'compact layout', - trigger: 'active', - class: 'pf-map-overlay-compact', - iconClass: ['fas', 'fa-fw', 'fa-compress'] - }, - connection: { - title: 'WH data', - trigger: 'hover', - class: 'pf-map-overlay-connection-wh', - iconClass: ['fas', 'fa-fw', 'fa-fighter-jet'], - hoverIntent: { - over: function(e){ - let map = getMapObjectFromOverlayIcon(this); - let MapUtil = require('app/map/util'); - let connections = MapUtil.searchConnectionsByScopeAndType(map, 'wh'); - let serverDate = Util.getServerTime(); - - // show connection overlays --------------------------------------------------- - for(let connection of connections){ - let createdTimestamp = connection.getParameter('created'); - let updatedTimestamp = connection.getParameter('updated'); - - let createdDate = Util.convertTimestampToServerTime(createdTimestamp); - let updatedDate = Util.convertTimestampToServerTime(updatedTimestamp); - - let createdDiff = Util.getTimeDiffParts(createdDate, serverDate); - let updatedDiff = Util.getTimeDiffParts(updatedDate, serverDate); - - // format overlay label - let labels = [ - ' ' + formatTimeParts(createdDiff), - ' ' + formatTimeParts(updatedDiff) - ]; - - // add label overlay ------------------------------------------------------ - connection.addOverlay([ - 'Label', - { - label: labels.join('
'), - id: config.connectionOverlayWhId, - cssClass: [config.componentOverlayClass, 'small'].join(' '), - location: 0.35 - } - ]); - } - }, - out: function(e){ - let map = getMapObjectFromOverlayIcon(this); - let MapUtil = require('app/map/util'); - let connections = MapUtil.searchConnectionsByScopeAndType(map, 'wh'); - - for(let connection of connections){ - connection.removeOverlays(config.connectionOverlayWhId); - } - } - } - }, - connectionEol: { - title: 'EOL timer', - trigger: 'hover', - class: 'pf-map-overlay-connection-eol', - iconClass: ['far', 'fa-fw', 'fa-clock'], - hoverIntent: { - over: function(e){ - let map = getMapObjectFromOverlayIcon(this); - let MapUtil = require('app/map/util'); - let connections = MapUtil.searchConnectionsByScopeAndType(map, 'wh', ['wh_eol']); - let serverDate = Util.getServerTime(); - - for(let connection of connections){ - let eolTimestamp = connection.getParameter('eolUpdated'); - let eolDate = Util.convertTimestampToServerTime(eolTimestamp); - let diff = Util.getTimeDiffParts(eolDate, serverDate); - - connection.addOverlay([ - 'Label', - { - label: ' ' + formatTimeParts(diff), - id: config.connectionOverlayEolId, - cssClass: [config.componentOverlayClass, 'eol'].join(' '), - location: 0.25 - } - ]); - } - }, - out: function(e){ - let map = getMapObjectFromOverlayIcon(this); - let MapUtil = require('app/map/util'); - let connections = MapUtil.searchConnectionsByScopeAndType(map, 'wh', ['wh_eol']); - - for(let connection of connections){ - connection.removeOverlay(config.connectionOverlayEolId); - } - } - } - } - }; - - /** - * get map overlay element by type e.g. timer/counter, info - overlay - * @param overlayType - * @returns {*} - */ - $.fn.getMapOverlay = function(overlayType){ - let mapWrapperElement = $(this).parents('.' + config.mapWrapperClass); - - let mapOverlay = null; - switch(overlayType){ - case 'timer': - mapOverlay = mapWrapperElement.find('.' + config.mapOverlayTimerClass); - break; - case 'info': - mapOverlay = mapWrapperElement.find('.' + config.mapOverlayInfoClass); - break; - case 'local': - mapOverlay = mapWrapperElement.find('.' + config.overlayLocalClass); - break; - } - - return mapOverlay; - }; - - /** - * draws the map update counter to the map overlay timer - * @param percent - * @param value - * @returns {*} - */ - $.fn.setMapUpdateCounter = function(percent, value){ - - let mapOverlayTimer = $(this); - - // check if counter already exists - let counterChart = mapOverlayTimer.getMapCounter(); - - if(counterChart.length === 0){ - // create new counter - - counterChart = $('
', { - class: [Init.classes.pieChart.class, Init.classes.pieChart.pieChartMapCounterClass].join(' ') - }).attr('data-percent', percent).append( - $('', { - text: value - }) - ); - - mapOverlayTimer.append(counterChart); - - // init counter - counterChart.initMapUpdateCounter(); - - // set tooltip - mapOverlayTimer.attr('data-placement', 'left'); - mapOverlayTimer.attr('title', 'update counter'); - mapOverlayTimer.tooltip(); - } - - return counterChart; - }; - - /** - * get the map counter chart from overlay - * @returns {JQuery|*|T|{}|jQuery} - */ - $.fn.getMapCounter = function(){ - return $(this).find('.' + Init.classes.pieChart.pieChartMapCounterClass); - }; - - $.fn.getMapOverlayInterval = function(){ - return $(this).getMapOverlay('timer').getMapCounter().data('interval'); - }; - - /** - * start the map update counter or reset - */ - $.fn.startMapUpdateCounter = function(){ - - let mapOverlayTimer = $(this); - let counterChart = mapOverlayTimer.getMapCounter(); - - let maxSeconds = config.logTimerCount; - - let counterChartLabel = counterChart.find('span'); - - let percentPerCount = 100 / maxSeconds; - - // update counter - let updateChart = function(tempSeconds){ - let pieChart = counterChart.data('easyPieChart'); - - if(pieChart !== undefined){ - counterChart.data('easyPieChart').update( percentPerCount * tempSeconds); - } - counterChartLabel.text(tempSeconds); - }; - - // main timer function is called on any counter update - let timer = function(mapUpdateCounter){ - // decrease timer - let currentSeconds = counterChart.data('currentSeconds'); - currentSeconds--; - counterChart.data('currentSeconds', currentSeconds); - - if(currentSeconds >= 0){ - // update counter - updateChart(currentSeconds); - }else{ - // hide counter and reset - clearInterval(mapUpdateCounter); - - mapOverlayTimer.velocity('transition.whirlOut', { - duration: Init.animationSpeed.mapOverlay, - complete: function(){ - counterChart.data('interval', false); - Util.getMapElementFromOverlay(mapOverlayTimer).trigger('pf:unlocked'); - } - }); - } - }; - - // get current seconds (in case the timer is already running) - let currentSeconds = counterChart.data('currentSeconds'); - - // start values for timer and chart - counterChart.data('currentSeconds', maxSeconds); - updateChart(maxSeconds); - - if( - currentSeconds === undefined || - currentSeconds < 0 - ){ - // start timer - let mapUpdateCounter = setInterval(() => { - timer(mapUpdateCounter); - }, 1000); - - // store counter interval - counterChart.data('interval', mapUpdateCounter); - - // show overlay - if(mapOverlayTimer.is(':hidden')){ - mapOverlayTimer.velocity('stop').velocity('transition.whirlIn', { duration: Init.animationSpeed.mapOverlay }); - } - } - }; - - /** - * update (show/hide) a overlay icon in the "info"-overlay - * show/hide the overlay itself is no icons are visible - * @param option - * @param viewType - */ - $.fn.updateOverlayIcon = function(option, viewType){ - let mapOverlayInfo = $(this); - - let showOverlay = false; - - let mapOverlayIconClass = options[option].class; - - // look for the overlay icon that should be updated - let iconElement = mapOverlayInfo.find('.' + mapOverlayIconClass); - - if(iconElement){ - if(viewType === 'show'){ - showOverlay = true; - - // check "trigger" and mark as "active" - if( - options[option].trigger === 'active' || - options[option].trigger === 'refresh' - ){ - iconElement.addClass('active'); - } - - // check if icon is not already visible - // -> prevents unnecessary "show" animation - if( !iconElement.data('visible') ){ - // display animation for icon - iconElement.velocity({ - opacity: [0.8, 0], - scale: [1, 0], - width: ['20px', 0], - height: ['20px', 0], - marginRight: ['10px', 0] - },{ - duration: 240, - easing: 'easeInOutQuad' - }); - - iconElement.data('visible', true); - } - }else if(viewType === 'hide'){ - // check if icon is not already visible - // -> prevents unnecessary "hide" animation - if(iconElement.data('visible')){ - iconElement.removeClass('active').velocity('reverse'); - iconElement.data('visible', false); - } - - // check if there is any visible icon remaining - let visibleIcons = mapOverlayInfo.find('i:visible'); - if(visibleIcons.length > 0){ - showOverlay = true; - } - } - } - - // show the entire overlay if there is at least one active icon - if( - showOverlay === true && - mapOverlayInfo.is(':hidden') - ){ - // show overlay - mapOverlayInfo.velocity('stop').velocity('transition.whirlIn', { duration: Init.animationSpeed.mapOverlay }); - }else if( - showOverlay === false && - mapOverlayInfo.is(':visible') - ){ - // hide overlay - mapOverlayInfo.velocity('stop').velocity('transition.whirlOut', { duration: Init.animationSpeed.mapOverlay }); - } - }; - - /** - * init all map overlays on a "parent" element - * @returns {*} - */ - $.fn.initMapOverlays = function(){ - return this.each(function(){ - let parentElement = $(this); - - let mapOverlayTimer = $('
', { - class: [config.mapOverlayClass, config.mapOverlayTimerClass].join(' ') - }); - parentElement.append(mapOverlayTimer); - - // ------------------------------------------------------------------------------------ - // add map overlay info. after scrollbar is initialized - let mapOverlayInfo = $('
', { - class: [config.mapOverlayClass, config.mapOverlayInfoClass].join(' ') - }); - - // add all overlay elements - for(let prop in options){ - if(options.hasOwnProperty(prop)){ - let icon = $('', { - class: options[prop].iconClass.concat( ['pull-right', options[prop].class] ).join(' ') - }).attr('title', options[prop].title).tooltip({ - placement: 'bottom', - container: 'body', - delay: 150 - }); - - // add "hover" action for some icons - if( - options[prop].trigger === 'hover' || - options[prop].trigger === 'refresh' - ){ - icon.hoverIntent(options[prop].hoverIntent); - } - - // add "click" handler for some icons - if(options[prop].hasOwnProperty('onClick')){ - icon.on('click', options[prop].onClick); - } - - mapOverlayInfo.append(icon); - } - } - parentElement.append(mapOverlayInfo); - - // reset map update timer - mapOverlayTimer.setMapUpdateCounter(100, config.logTimerCount); - }); - }; - -}); \ No newline at end of file diff --git a/js/app/map/overlay/overlay.js b/js/app/map/overlay/overlay.js new file mode 100644 index 00000000..8fdb7c81 --- /dev/null +++ b/js/app/map/overlay/overlay.js @@ -0,0 +1,892 @@ +/** + * map overlay functions + */ + +define([ + 'jquery', + 'app/init', + 'app/util', + 'app/map/overlay/util', + 'app/map/util' +], ($, Init, Util, MapOverlayUtil, MapUtil) => { + 'use strict'; + + /** + * get map object (jsPlumb) from iconElement + * @param overlayIcon + * @returns {*} + */ + let getMapObjectFromOverlayIcon = overlayIcon => { + return MapUtil.getMapInstance(Util.getMapElementFromOverlay(overlayIcon).data('id')); + }; + + /** + * add/update endpoints with overlays from signature mapping + * @param endpoint + * @param labelData + */ + let updateEndpointOverlaySignatureLabel = (endpoint, labelData) => { + let labels = labelData.labels; + let names = labelData.names; + let overlay = endpoint.getOverlay(MapOverlayUtil.config.endpointOverlayId); + + if(overlay instanceof jsPlumb.Overlays.Label){ + // update existing overlay + if( + !labels.equalValues(overlay.getParameter('signatureLabels')) || + !names.equalValues(overlay.getParameter('signatureNames')) + ){ + // update label only on label changes + overlay.setLabel(MapUtil.formatEndpointOverlaySignatureLabel(labels)); + overlay.setParameter('fullSize', false); + overlay.setParameter('signatureLabels', labels); + overlay.setParameter('signatureNames', names); + overlay.updateClasses(labels.length ? 'small' : 'icon', labels.length ? 'icon' : 'small'); + overlay.setLocation(MapUtil.getEndpointOverlaySignatureLocation(endpoint, labels)); + } + }else{ + // add new overlay + endpoint.addOverlay([ + 'Label', + { + label: MapUtil.formatEndpointOverlaySignatureLabel(labels), + id: MapOverlayUtil.config.endpointOverlayId, + cssClass: [MapOverlayUtil.config.componentOverlayClass, labels.length ? 'small' : 'icon'].join(' '), + location: MapUtil.getEndpointOverlaySignatureLocation(endpoint, labels), + events: { + toggleSize: function(fullSize){ + let signatureNames = this.getParameter('signatureNames'); + if(fullSize && !this.getParameter('fullSize') && signatureNames.length){ + this.setLabel(this.getLabel() + '
' + '' + signatureNames.join(', ') + ''); + this.setParameter('fullSize', true); + }else if(this.getParameter('fullSize')){ + this.setLabel(MapUtil.formatEndpointOverlaySignatureLabel(this.getParameter('signatureLabels'))); + this.setParameter('fullSize', false); + } + } + }, + parameters: { + fullSize: false, + signatureLabels: labels, + signatureNames: names + } + } + ]); + } + }; + + /** + * get overlay parameters for connection overlay (type 'diamond' or 'arrow') + * @param overlayType + * @param direction + * @returns {{length: number, foldback: number, direction: number}} + */ + let getConnectionArrowOverlayParams = (overlayType, direction = 1) => { + switch(overlayType){ + case 'arrow': + return { + length: 15, + direction: direction, + foldback: 0.8 + }; + default: // diamond + return { + length: 10, + direction: 1, + foldback: 2 + }; + } + }; + + /** + * add overlays to connections (signature based data) + * @param map + * @param connectionsData + */ + let updateInfoSignatureOverlays = (map, connectionsData) => { + let type = 'info_signature'; + connectionsData = Util.arrayToObject(connectionsData); + + map.batch(() => { + map.getAllConnections().forEach(connection => { + let connectionId = connection.getParameter('connectionId'); + let sourceEndpoint = connection.endpoints[0]; + let targetEndpoint = connection.endpoints[1]; + + let connectionData = connectionsData.hasOwnProperty(connectionId) ? connectionsData[connectionId] : undefined; + let signatureTypeData = MapUtil.getConnectionDataFromSignatures(connection, connectionData); + + let sizeLockedBySignature = false; + + if(connection.scope === 'wh'){ + if(!connection.hasType(type)){ + connection.addType(type); + } + + let overlayArrow = connection.getOverlay(MapOverlayUtil.config.connectionOverlayArrowId); + + // Arrow overlay needs to be cleared() (removed) if 'info_signature' gets removed! + // jsPlumb does not handle overlay updates for Arrow overlays... so we need to re-apply the the overlay manually + if(overlayArrow.path && !overlayArrow.path.isConnected){ + connection.canvas.appendChild(overlayArrow.path); + } + + // since there "could" be multiple sig labels on each endpoint, + // there can only one "primary label picked up for wormhole jump mass detection! + let primLabel; + + let overlayType = 'diamond'; // not specified + let arrowDirection = 1; + + if(connectionData && connectionData.signatures){ + // signature data found for current connection + let sourceLabel = signatureTypeData.source.labels; + let targetLabel = signatureTypeData.target.labels; + + // add arrow (connection) overlay that points from "XXX" => "K162" ---------------------------- + if( + (sourceLabel.includes('K162') && targetLabel.includes('K162')) || + (sourceLabel.length === 0 && targetLabel.length === 0) || + ( + sourceLabel.length > 0 && targetLabel.length > 0 && + !sourceLabel.includes('K162') && !targetLabel.includes('K162') + ) + ){ + // unknown direction -> show default 'diamond' overlay + overlayType = 'diamond'; + }else if( + (sourceLabel.includes('K162')) || + (sourceLabel.length === 0 && !targetLabel.includes('K162')) + ){ + // convert default arrow direction + overlayType = 'arrow'; + arrowDirection = -1; + + primLabel = targetLabel.find(label => label !== 'K162'); + }else{ + // default arrow direction is fine + overlayType = 'arrow'; + + primLabel = sourceLabel.find(label => label !== 'K162'); + } + } + + // class changes must be done on "connection" itself not on "overlayArrow" + // -> because Arrow might not be rendered to map at this point (if it does not exist already) + if(overlayType === 'arrow'){ + connection.updateClasses( + MapOverlayUtil.config.connectionArrowOverlaySuccessClass, + MapOverlayUtil.config.connectionArrowOverlayDangerClass + ); + }else{ + connection.updateClasses( + MapOverlayUtil.config.connectionArrowOverlayDangerClass, + MapOverlayUtil.config.connectionArrowOverlaySuccessClass + ); + } + + overlayArrow.updateFrom(getConnectionArrowOverlayParams(overlayType, arrowDirection)); + + // update/add endpoint overlays ------------------------------------------------------------------- + updateEndpointOverlaySignatureLabel(sourceEndpoint, signatureTypeData.source); + updateEndpointOverlaySignatureLabel(targetEndpoint, signatureTypeData.target); + + // fix/overwrite existing jump mass connection type ----------------------------------------------- + // if a connection type for "jump mass" (e.g. S, M, L, XL) is set for this connection + // we should check/compare it with the current primary signature label from signature mapping + // and change it if necessary + if(Init.wormholes.hasOwnProperty(primLabel)){ + // connection size from mapped signature + sizeLockedBySignature = true; + + let wormholeData = Object.assign({}, Init.wormholes[primLabel]); + // get 'connection mass type' from wormholeData + let massType = Util.getObjVal(wormholeData, 'size.type'); + + if(massType && !connection.hasType(massType)){ + MapOverlayUtil.getMapOverlay(connection.canvas, 'timer').startMapUpdateCounter(); + MapUtil.setConnectionJumpMassType(connection, wormholeData.size.type); + MapUtil.markAsChanged(connection); + } + } + }else{ + // connection is not 'wh' scope + if(connection.hasType(type)){ + connection.removeType(type); + } + } + + // lock/unlock connection for manual size changes (from contextmenu) + connection.setParameter('sizeLocked', sizeLockedBySignature); + }); + }); + }; + + /** + * format json object with "time parts" into string + * @param parts + * @returns {string} + */ + let formatTimeParts = parts => { + let label = ''; + if(parts.days){ + label += parts.days + 'd '; + } + label += ('00' + parts.hours).slice(-2); + label += ':' + ('00' + parts.min).slice(-2); + return label; + }; + + /** + * hide default icon and replace it with "loading" icon + * @param iconElement + */ + let showLoading = iconElement => { + iconElement = $(iconElement); + let dataName = 'default-icon'; + let defaultIconClass = iconElement.data(dataName); + + // get default icon class + if( !defaultIconClass ){ + // index 0 == 'fa-fw', index 1 == IconName + defaultIconClass = $(iconElement).attr('class').match(/\bfa-\S*/g)[1]; + iconElement.data(dataName, defaultIconClass); + } + + iconElement.toggleClass( defaultIconClass + ' fa-sync fa-spin'); + }; + + /** + * hide "loading" icon and replace with default icon + * @param iconElement + */ + let hideLoading = iconElement => { + iconElement = $(iconElement); + let dataName = 'default-icon'; + let defaultIconClass = iconElement.data(dataName); + + iconElement.toggleClass( defaultIconClass + ' fa-sync fa-spin'); + }; + + /** + * get overlay icon from e.g. mapElement + * @param element + * @param iconClass + * @param overlayType + * @returns {*} + */ + let getOverlayIcon = (element, iconClass, overlayType = 'info') => { + return MapOverlayUtil.getMapOverlay(element, overlayType).find('.' + iconClass); + }; + + /** + * showInfoSignatureOverlays + * -> used by "refresh" overlays (hover) AND/OR initial menu trigger + */ + let showInfoSignatureOverlays = mapElement => { + let mapId = mapElement.data('id'); + let map = MapUtil.getMapInstance(mapId); + let mapData = Util.getCurrentMapData(mapId); + let connectionsData = Util.getObjVal(mapData, 'data.connections'); + + if(connectionsData){ + let overlayIcon = getOverlayIcon(mapElement, options.mapSignatureOverlays.class); + showLoading(overlayIcon); + updateInfoSignatureOverlays(map, connectionsData); + hideLoading(overlayIcon); + } + }; + + /** + * hideInfoSignatureOverlays + * -> see showInfoSignatureOverlays() + */ + let hideInfoSignatureOverlays = mapElement => { + let mapId = mapElement.data('id'); + let map = MapUtil.getMapInstance(mapId); + let type = 'info_signature'; + + map.batch(() => { + map.getAllConnections().forEach(connection => { + let overlayArrow = connection.getOverlay(MapOverlayUtil.config.connectionOverlayArrowId); + + if(overlayArrow){ + overlayArrow.cleanup(); + } + + if(connection.hasType(type)){ + connection.removeType(type, {}, true); + } + }); + + map.selectEndpoints().removeOverlay(MapOverlayUtil.config.endpointOverlayId); + }); + }; + + /** + * Overlay options (all available map options shown in overlay) + * "active": (active || hover) indicated whether an icon/option + * is marked as "active". + * "active": Makes icon active when visible + * "hover": Make icon active on hover + */ + let options = { + filter: { + title: 'active filter', + trigger: 'active', + class: 'pf-map-overlay-filter', + iconClass: ['fas', 'fa-fw', 'fa-filter'], + onClick: function(e){ + // clear all filter + let mapElement = Util.getMapElementFromOverlay(this); + let map = getMapObjectFromOverlayIcon(this); + + MapUtil.storeLocalData('map', mapElement.data('id'), 'filterScopes', []); + MapUtil.filterMapByScopes(map, []); + } + }, + mapSnapToGrid: { + title: 'active grid', + trigger: 'active', + class: 'pf-map-overlay-grid', + iconClass: ['fas', 'fa-fw', 'fa-th'] + }, + mapMagnetizer: { + title: 'active magnetizer', + trigger: 'active', + class: 'pf-map-overlay-magnetizer', + iconClass: ['fas', 'fa-fw', 'fa-magnet'] + }, + systemRegion: { + title: 'show regions', + trigger: 'hover', + class: 'pf-map-overlay-region', + iconClass: ['fas', 'fa-fw', 'fa-tags'], + hoverIntent: { + over: function(e){ + let mapElement = Util.getMapElementFromOverlay(this); + mapElement.find('.' + MapOverlayUtil.config.systemHeadClass).each(function(){ + let systemHead = $(this); + // init popover if not already exists + if(!systemHead.data('bs.popover')){ + let system = systemHead.parent(); + systemHead.popover({ + placement: 'right', + html: true, + trigger: 'manual', + container: mapElement, + title: false, + content: Util.getSystemRegionTable( + system.data('region'), + system.data('faction') || null + ) + }); + } + systemHead.setPopoverSmall(); + systemHead.popover('show'); + }); + }, + out: function(e){ + let mapElement = Util.getMapElementFromOverlay(this); + mapElement.find('.' + MapOverlayUtil.config.systemHeadClass).popover('hide'); + } + } + }, + mapSignatureOverlays: { + title: 'active signature overlays', + trigger: 'active', + class: 'pf-map-overlay-endpoint', + iconClass: ['fas', 'fa-fw', 'fa-link'] + }, + mapCompact: { + title: 'compact layout', + trigger: 'active', + class: 'pf-map-overlay-compact', + iconClass: ['fas', 'fa-fw', 'fa-compress'] + }, + connection: { + title: 'WH data', + trigger: 'hover', + class: 'pf-map-overlay-connection-wh', + iconClass: ['fas', 'fa-fw', 'fa-fighter-jet'], + hoverIntent: { + over: function(e){ + let map = getMapObjectFromOverlayIcon(this); + let connections = MapUtil.searchConnectionsByScopeAndType(map, 'wh'); + let serverDate = Util.getServerTime(); + + // show connection overlays ----------------------------------------------------------------------- + for(let connection of connections){ + let createdTimestamp = connection.getParameter('created'); + let updatedTimestamp = connection.getParameter('updated'); + + let createdDate = Util.convertTimestampToServerTime(createdTimestamp); + let updatedDate = Util.convertTimestampToServerTime(updatedTimestamp); + + let createdDiff = Util.getTimeDiffParts(createdDate, serverDate); + let updatedDiff = Util.getTimeDiffParts(updatedDate, serverDate); + + // format overlay label + let labels = [ + formatTimeParts(createdDiff) + ' ', + formatTimeParts(updatedDiff) + ' ' + ]; + + // add label overlay -------------------------------------------------------------------------- + connection.addOverlay([ + 'Label', + { + label: labels.join('
'), + id: MapOverlayUtil.config.connectionOverlayWhId, + cssClass: [MapOverlayUtil.config.componentOverlayClass, 'small', 'text-right'].join(' '), + location: 0.35 + } + ]); + } + }, + out: function(e){ + let map = getMapObjectFromOverlayIcon(this); + let connections = MapUtil.searchConnectionsByScopeAndType(map, 'wh'); + + for(let connection of connections){ + connection.removeOverlay(MapOverlayUtil.config.connectionOverlayWhId); + } + } + } + }, + connectionEol: { + title: 'EOL timer', + trigger: 'hover', + class: 'pf-map-overlay-connection-eol', + iconClass: ['fas', 'fa-fw', 'fa-hourglass-end'], + hoverIntent: { + over: function(e){ + let map = getMapObjectFromOverlayIcon(this); + let connections = MapUtil.searchConnectionsByScopeAndType(map, 'wh', ['wh_eol']); + let serverDate = Util.getServerTime(); + + for(let connection of connections){ + let eolTimestamp = connection.getParameter('eolUpdated'); + let eolDate = Util.convertTimestampToServerTime(eolTimestamp); + let diff = Util.getTimeDiffParts(eolDate, serverDate); + + connection.addOverlay([ + 'Label', + { + label: ' ' + formatTimeParts(diff), + id: MapOverlayUtil.config.connectionOverlayEolId, + cssClass: [MapOverlayUtil.config.componentOverlayClass, 'eol'].join(' '), + location: 0.25 + } + ]); + } + }, + out: function(e){ + let map = getMapObjectFromOverlayIcon(this); + let connections = MapUtil.searchConnectionsByScopeAndType(map, 'wh', ['wh_eol']); + + for(let connection of connections){ + connection.removeOverlay(MapOverlayUtil.config.connectionOverlayEolId); + } + } + } + } + }; + + /** + * draws the map update counter to the map overlay timer + * @param mapOverlayTimer + * @param percent + * @param value + * @returns {*} + */ + let setMapUpdateCounter = (mapOverlayTimer, percent, value) => { + // check if counter already exists + let counterChart = MapOverlayUtil.getMapCounter(mapOverlayTimer); + + if(counterChart.length === 0){ + // create new counter + + counterChart = $('
', { + class: [Init.classes.pieChart.class, Init.classes.pieChart.pieChartMapCounterClass].join(' ') + }).attr('data-percent', percent).append( + $('', { + text: value + }) + ); + + mapOverlayTimer.append(counterChart); + + // init counter + counterChart.initMapUpdateCounter(); + + // set tooltip + mapOverlayTimer.attr('data-placement', 'left'); + mapOverlayTimer.attr('title', 'update counter'); + mapOverlayTimer.tooltip(); + } + + return counterChart; + }; + + /** + * start the map update counter or reset + */ + $.fn.startMapUpdateCounter = function(){ + let mapOverlayTimer = $(this); + let counterChart = MapOverlayUtil.getMapCounter(mapOverlayTimer); + + let maxSeconds = MapOverlayUtil.config.logTimerCount; + + let counterChartLabel = counterChart.find('span'); + + let percentPerCount = 100 / maxSeconds; + + // update counter + let updateChart = tempSeconds => { + let pieChart = counterChart.data('easyPieChart'); + + if(pieChart !== undefined){ + counterChart.data('easyPieChart').update( percentPerCount * tempSeconds); + } + counterChartLabel.text(tempSeconds); + }; + + // main timer function is called on any counter update + let timer = mapUpdateCounter => { + // decrease timer + let currentSeconds = counterChart.data('currentSeconds'); + currentSeconds--; + counterChart.data('currentSeconds', currentSeconds); + + if(currentSeconds >= 0){ + // update counter + updateChart(currentSeconds); + }else{ + // hide counter and reset + clearInterval(mapUpdateCounter); + + mapOverlayTimer.velocity('transition.whirlOut', { + duration: Init.animationSpeed.mapOverlay, + complete: function(){ + counterChart.data('interval', false); + Util.getMapElementFromOverlay(mapOverlayTimer).trigger('pf:unlocked'); + } + }); + } + }; + + // get current seconds (in case the timer is already running) + let currentSeconds = counterChart.data('currentSeconds'); + + // start values for timer and chart + counterChart.data('currentSeconds', maxSeconds); + updateChart(maxSeconds); + + if( + currentSeconds === undefined || + currentSeconds < 0 + ){ + // start timer + let mapUpdateCounter = setInterval(() => { + timer(mapUpdateCounter); + }, 1000); + + // store counter interval + counterChart.data('interval', mapUpdateCounter); + + // show overlay + if(mapOverlayTimer.is(':hidden')){ + mapOverlayTimer.velocity('stop').velocity('transition.whirlIn', { duration: Init.animationSpeed.mapOverlay }); + } + } + }; + + /** + * update (show/hide) a overlay icon in the "info"-overlay + * show/hide the overlay itself is no icons are visible + * @param option + * @param viewType + */ + $.fn.updateOverlayIcon = function(option, viewType){ + let mapOverlayInfo = $(this); + + let showOverlay = false; + + let mapOverlayIconClass = options[option].class; + + // look for the overlay icon that should be updated + let iconElement = mapOverlayInfo.find('.' + mapOverlayIconClass); + + if(iconElement){ + if(viewType === 'show'){ + showOverlay = true; + + // check "trigger" and mark as "active" + if( + options[option].trigger === 'active' || + options[option].trigger === 'refresh' + ){ + iconElement.addClass('active'); + } + + // check if icon is not already visible + // -> prevents unnecessary "show" animation + if( !iconElement.data('visible') ){ + // display animation for icon + iconElement.velocity({ + opacity: [0.8, 0], + scale: [1, 0], + width: ['20px', 0], + height: ['20px', 0], + marginRight: ['10px', 0] + },{ + duration: 240, + easing: 'easeInOutQuad' + }); + + iconElement.data('visible', true); + } + }else if(viewType === 'hide'){ + // check if icon is not already visible + // -> prevents unnecessary "hide" animation + if(iconElement.data('visible')){ + iconElement.removeClass('active').velocity('reverse'); + iconElement.data('visible', false); + } + + // check if there is any visible icon remaining + let visibleIcons = mapOverlayInfo.find('i:visible'); + if(visibleIcons.length > 0){ + showOverlay = true; + } + } + } + + // show the entire overlay if there is at least one active icon + if( + showOverlay === true && + mapOverlayInfo.is(':hidden') + ){ + // show overlay + mapOverlayInfo.velocity('stop').velocity('transition.whirlIn', { duration: Init.animationSpeed.mapOverlay }); + }else if( + showOverlay === false && + mapOverlayInfo.is(':visible') + ){ + // hide overlay + mapOverlayInfo.velocity('stop').velocity('transition.whirlOut', { duration: Init.animationSpeed.mapOverlay }); + } + }; + + /** + * update map zoom overlay information + * @param map + */ + let updateZoomOverlay = map => { + let zoom = map.getZoom(); + let zoomPercent = Math.round(zoom * 1000) / 10; + let zoomOverlay = MapOverlayUtil.getMapOverlay(map.getContainer(), 'zoom'); + let zoomValue = zoomOverlay.find('.' + MapOverlayUtil.config.zoomOverlayValueClass); + let zoomUp = zoomOverlay.find('.' + MapOverlayUtil.config.zoomOverlayUpClass); + let zoomDown = zoomOverlay.find('.' + MapOverlayUtil.config.zoomOverlayDownClass); + zoomValue.toggleClass('active', zoom !== 1).text(zoomPercent); + zoomUp.toggleClass('disabled', zoom >= MapUtil.config.zoomMax); + zoomDown.toggleClass('disabled', zoom <= MapUtil.config.zoomMin); + }; + + /** + * map debug overlays for connections/endpoints + * -> requires manual added "debug" GET param to URL + * @param map + */ + let initMapDebugOverlays = map => { + let url = new URL(window.location.href); + if(url.searchParams.has('debug')){ + let mapContainer = $(map.getContainer()); + + // debug map overlays for connection/endpoints + mapContainer.on('mouseover', '.jtk-connector', function(e){ + e.stopPropagation(); + + if(e.target.classList.contains('jtk-connector-outline')){ + let connection = e.currentTarget._jsPlumb; + // show debug overlay only if there is no active debug + if(!connection.getOverlay(MapOverlayUtil.config.debugOverlayId)){ + // find nearby connections + let connections = []; + let endpoints = []; + let hasComponentId = id => { + return component => component.id === id; + }; + + for(let endpoint of connection.endpoints){ + let connectionsInfo = map.anchorManager.getConnectionsFor(endpoint.elementId); + for(let connectionInfo of connectionsInfo){ + if(!connections.some(hasComponentId(connectionInfo[0].id))){ + connections.push(connectionInfo[0]); + for(let endpointTemp of connectionInfo[0].endpoints){ + if(!endpoints.some(hasComponentId(endpointTemp.id))){ + endpoints.push(endpointTemp); + } + } + } + } + } + + let createConnectionOverlay = connection => { + let data = MapUtil.getDataByConnection(connection); + + let html = '
'; + html += ''; + html += ''; + html += ''; + html += '
' + connection.id + '' + data.id + '
Scope:' + data.scope + '
Type:' + data.type.toString() + '
'; + + return $(html).on('click', function(){ + console.info(connection); + }); + }; + + for(let connection of connections){ + connection.addOverlay([ + 'Custom', + { + id: MapOverlayUtil.config.debugOverlayId, + cssClass: [MapOverlayUtil.config.componentOverlayClass, 'debug'].join(' '), + create: createConnectionOverlay + } + ]); + } + + let createEndpointOverlay = endpoint => { + let types = MapUtil.filterDefaultTypes(endpoint.getType()); + + let html = '
'; + html += ''; + html += ''; + html += ''; + html += '
' + endpoint.id + '
Scope:' + endpoint.scope + '
Type:' + types.toString() + '
'; + + return $(html).on('click', function(){ + console.info(endpoint); + }); + }; + + for(let endpoint of endpoints){ + endpoint.addOverlay([ + 'Custom', + { + id: MapOverlayUtil.config.debugOverlayId, + cssClass: [MapOverlayUtil.config.componentOverlayClass, 'debug'].join(' '), + create: createEndpointOverlay + } + ]); + } + } + } + }); + + mapContainer.on('mouseover', function(e){ + e.stopPropagation(); + if(e.target === e.currentTarget){ + map.select().removeOverlay(MapOverlayUtil.config.debugOverlayId); + map.selectEndpoints().removeOverlay(MapOverlayUtil.config.debugOverlayId); + } + }); + } + }; + + /** + * init map zoom overlay + * @returns {jQuery} + */ + let initZoomOverlay = () => { + let clickHandler = e => { + let zoomIcon = $(e.target); + if(!zoomIcon.hasClass('disabled')){ + let zoomAction = zoomIcon.attr('data-zoom'); + let map = getMapObjectFromOverlayIcon(zoomIcon); + MapUtil.changeZoom(map, zoomAction); + } + }; + + return $('
', { + class: [MapOverlayUtil.config.mapOverlayClass, MapOverlayUtil.config.mapOverlayZoomClass].join(' ') + }).append( + $('', { + class: ['fas', 'fa-caret-up', MapOverlayUtil.config.zoomOverlayUpClass].join(' ') + }).attr('data-zoom', 'up').on('click', clickHandler), + $('', { + class: MapOverlayUtil.config.zoomOverlayValueClass, + text: '100' + }), + $('', { + class: ['fas', 'fa-caret-down', MapOverlayUtil.config.zoomOverlayDownClass].join(' ') + }).attr('data-zoom', 'down').on('click', clickHandler) + ); + }; + + /** + * init all map overlays on a "parent" element + * @returns {*} + */ + $.fn.initMapOverlays = function(){ + return this.each(function(){ + let parentElement = $(this); + + let mapOverlayTimer = $('
', { + class: [MapOverlayUtil.config.mapOverlayClass, MapOverlayUtil.config.mapOverlayTimerClass].join(' ') + }); + parentElement.append(mapOverlayTimer); + + + parentElement.append(initZoomOverlay()); + + // -------------------------------------------------------------------------------------------------------- + // add map overlay info. after scrollbar is initialized + let mapOverlayInfo = $('
', { + class: [MapOverlayUtil.config.mapOverlayClass, MapOverlayUtil.config.mapOverlayInfoClass].join(' ') + }); + + // add all overlay elements + for(let prop in options){ + if(options.hasOwnProperty(prop)){ + let icon = $('', { + class: options[prop].iconClass.concat( ['pull-right', options[prop].class] ).join(' ') + }).attr('title', options[prop].title).tooltip({ + placement: 'bottom', + container: 'body', + delay: 150 + }); + + // add "hover" action for some icons + if( + options[prop].trigger === 'hover' || + options[prop].trigger === 'refresh' + ){ + icon.hoverIntent(options[prop].hoverIntent); + } + + // add "click" handler for some icons + if(options[prop].hasOwnProperty('onClick')){ + icon.on('click', options[prop].onClick); + } + + mapOverlayInfo.append(icon); + } + } + parentElement.append(mapOverlayInfo); + + // reset map update timer + setMapUpdateCounter(mapOverlayTimer, 100, MapOverlayUtil.config.logTimerCount); + }); + }; + + return { + showInfoSignatureOverlays: showInfoSignatureOverlays, + hideInfoSignatureOverlays: hideInfoSignatureOverlays, + updateZoomOverlay: updateZoomOverlay, + initMapDebugOverlays: initMapDebugOverlays + }; +}); \ No newline at end of file diff --git a/js/app/map/overlay/util.js b/js/app/map/overlay/util.js new file mode 100644 index 00000000..21fc49ab --- /dev/null +++ b/js/app/map/overlay/util.js @@ -0,0 +1,97 @@ +/** + * map overlay util functions + */ + +define([ + 'jquery', + 'app/init', + 'app/util', + 'app/map/util' +], ($, Init, Util) => { + 'use strict'; + + let config = { + logTimerCount: 3, // map log timer in seconds + + mapWrapperClass: 'pf-map-wrapper', // wrapper div (scrollable) + + // map overlays sections + mapOverlayClass: 'pf-map-overlay', // class for all map overlays + mapOverlayTimerClass: 'pf-map-overlay-timer', // class for map overlay timer e.g. map timer + mapOverlayZoomClass: 'pf-map-overlay-zoom', // class for map overlay zoom + mapOverlayInfoClass: 'pf-map-overlay-info', // class for map overlay info e.g. map info + overlayLocalClass: 'pf-map-overlay-local', // class for map overlay "local" table + + // system + systemHeadClass: 'pf-system-head', // class for system head + + // connection overlay ids (they are not unique like CSS ids!) + connectionOverlayArrowId: 'pf-map-connection-arrow-overlay', // connection Arrows overlay ID (jsPlumb) + connectionOverlayWhId: 'pf-map-connection-wh-overlay', // connection WH overlay ID (jsPlumb) + connectionOverlayEolId: 'pf-map-connection-eol-overlay', // connection EOL overlay ID (jsPlumb) + + debugOverlayId: 'pf-map-debug-overlay', // connection/endpoint overlay ID (jsPlumb) + + endpointOverlayId: 'pf-map-endpoint-overlay', // endpoint overlay ID (jsPlumb) + + // connection overlay classes classes + componentOverlayClass: 'pf-map-component-overlay', // class for "normal size" overlay + + connectionArrowOverlaySuccessClass: 'pf-map-connection-arrow-overlay-success', // class for "success" arrow overlays + connectionArrowOverlayDangerClass: 'pf-map-connection-arrow-overlay-danger', // class for "danger" arrow overlays + + // zoom overlay + zoomOverlayUpClass: 'pf-zoom-overlay-up', + zoomOverlayDownClass: 'pf-zoom-overlay-down', + zoomOverlayValueClass: 'pf-zoom-overlay-value' + }; + + /** + * get map overlay element by type e.g. timer/counter, info - overlay + * @param element + * @param overlayType + * @returns {null} + */ + let getMapOverlay = (element, overlayType) => { + let mapWrapperElement = $(element).parents('.' + config.mapWrapperClass); + + let mapOverlay = null; + switch(overlayType){ + case 'timer': + mapOverlay = mapWrapperElement.find('.' + config.mapOverlayTimerClass); + break; + case 'info': + mapOverlay = mapWrapperElement.find('.' + config.mapOverlayInfoClass); + break; + case 'zoom': + mapOverlay = mapWrapperElement.find('.' + config.mapOverlayZoomClass); + break; + case 'local': + mapOverlay = mapWrapperElement.find('.' + config.overlayLocalClass); + break; + } + + return mapOverlay; + }; + + /** + * get the map counter chart from overlay + * @param element + * @returns {jQuery} + */ + let getMapCounter = element => $(element).find('.' + Init.classes.pieChart.pieChartMapCounterClass); + + /** + * get interval value from map timer overlay + * @param element + * @returns {*} + */ + let getMapOverlayInterval = element => getMapCounter(getMapOverlay(element, 'timer')).data('interval'); + + return { + config: config, + getMapOverlay: getMapOverlay, + getMapCounter: getMapCounter, + getMapOverlayInterval: getMapOverlayInterval + }; +}); \ No newline at end of file diff --git a/js/app/map/scrollbar.js b/js/app/map/scrollbar.js index 4f90b6ea..6d983d43 100644 --- a/js/app/map/scrollbar.js +++ b/js/app/map/scrollbar.js @@ -34,8 +34,9 @@ define([ }, advanced: { + autoUpdateTimeout: 120, // auto-update timeout (default: 60) updateOnContentResize: true, - autoExpandHorizontalScroll: true, + autoExpandHorizontalScroll: false, // on resize css scale() scroll content should not change //autoExpandHorizontalScroll: 2, autoScrollOnFocus: 'div', }, @@ -72,25 +73,24 @@ define([ /** * scroll to a specific position on map * demo: http://manos.malihu.gr/repository/custom-scrollbar/demo/examples/scrollTo_demo.html + * @param scrollWrapper * @param position + * @param options */ - $.fn.scrollToPosition = function(position){ - return this.each(function(){ - $(this).mCustomScrollbar('scrollTo', position); - }); + let scrollToPosition = (scrollWrapper, position, options) => { + $(scrollWrapper).mCustomScrollbar('scrollTo', position, options); }; /** * scroll to a specific system on map * -> subtract some offset for tooltips/connections + * @param scrollWrapper * @param position - * @returns {*} + * @param options */ - $.fn.scrollToSystem = function(position){ + let scrollToSystem = (scrollWrapper, position, options) => { position = getOffsetPosition(position, {x: -15, y: -35}); - return this.each(function(){ - $(this).mCustomScrollbar('scrollTo', position); - }); + scrollToPosition(scrollWrapper, position, options); }; /** @@ -106,4 +106,9 @@ define([ y: Math.max(0, position.y + offset.y) }; }; + + return { + scrollToPosition: scrollToPosition, + scrollToSystem: scrollToSystem + }; }); \ No newline at end of file diff --git a/js/app/map/system.js b/js/app/map/system.js index b6187040..beffcfc1 100644 --- a/js/app/map/system.js +++ b/js/app/map/system.js @@ -9,8 +9,9 @@ define([ 'app/util', 'bootbox', 'app/map/util', - 'app/map/layout' -], ($, Init, Util, bootbox, MapUtil, Layout) => { + 'app/map/layout', + 'app/map/magnetizing' +], ($, Init, Util, bootbox, MapUtil, Layout, Magnetizer) => { 'use strict'; let config = { @@ -39,6 +40,7 @@ define([ dialogSystemSectionInfoId: 'pf-system-dialog-section-info', // id for "info" section element dialogSystemSectionInfoStatusId: 'pf-system-dialog-section-info-status', // id for "status" message in "info" element dialogSystemAliasId: 'pf-system-dialog-alias', // id for "alias" static element + dialogSystemSignaturesId: 'pf-system-dialog-signatures', // id for "signatures" count static element dialogSystemDescriptionId: 'pf-system-dialog-description', // id for "description" static element dialogSystemCreatedId: 'pf-system-dialog-created', // id for "created" static element dialogSystemUpdatedId: 'pf-system-dialog-updated', // id for "updated" static element @@ -84,6 +86,7 @@ define([ let statusId = false; // -> no value change let alias = labelEmpty; + let signaturesCount = 0; let description = labelEmpty; let createdTime = labelUnknown; let updatedTime = labelUnknown; @@ -95,6 +98,7 @@ define([ info = labelExist; statusId = parseInt(Util.getObjVal(systemData, 'status.id')) || statusId; alias = systemData.alias.length ? Util.htmlEncode(systemData.alias) : alias; + signaturesCount = (Util.getObjVal(systemData, 'signatures') || []).length; description = systemData.description.length ? systemData.description : description; let dateCreated = new Date(systemData.created.created * 1000); @@ -116,6 +120,7 @@ define([ dialogElement.find('#' + config.dialogSystemStatusSelectId).val(statusId).trigger('change'); } dialogElement.find('#' + config.dialogSystemAliasId).html(alias); + dialogElement.find('#' + config.dialogSystemSignaturesId).toggleClass('txt-color-green', signaturesCount > 0).html(signaturesCount); dialogElement.find('#' + config.dialogSystemDescriptionId).html(description); dialogElement.find('#' + config.dialogSystemCreatedId).html(' ' + createdTime); dialogElement.find('#' + config.dialogSystemUpdatedId).html(' ' + updatedTime); @@ -172,22 +177,29 @@ define([ sectionInfoId: config.dialogSystemSectionInfoId, sectionInfoStatusId: config.dialogSystemSectionInfoStatusId, aliasId: config.dialogSystemAliasId, + signaturesId: config.dialogSystemSignaturesId, descriptionId: config.dialogSystemDescriptionId, createdId: config.dialogSystemCreatedId, updatedId: config.dialogSystemUpdatedId, statusData: statusData }; - // set current position as "default" system to add ------------------------------------------------------------ - let currentCharacterLog = Util.getCurrentCharacterLog(); + // check for pre-selected system ------------------------------------------------------------------------------ + let systemData; + if(options.systemData){ + systemData = options.systemData; + }else{ + // ... check for current active system (characterLog) ----------------------------------------------------- + let currentCharacterLog = Util.getCurrentCharacterLog(); + if(currentCharacterLog !== false){ + // set system from 'characterLog' data as pre-selected system + systemData = Util.getObjVal(currentCharacterLog, 'system'); + } + } - if( - currentCharacterLog !== false && - mapSystemIds.indexOf( currentCharacterLog.system.id ) === -1 - ){ - // current system is NOT already on this map - // set current position as "default" system to add - data.currentSystem = currentCharacterLog.system; + // check if pre-selected system is NOT already on this map + if(mapSystemIds.indexOf(Util.getObjVal(systemData, 'id')) === -1){ + data.currentSystem = systemData; } requirejs(['text!templates/dialog/system.html', 'mustache'], (template, Mustache) => { @@ -234,7 +246,7 @@ define([ // get new position newPosition = calculateNewSystemPosition(sourceSystem); - }else{ + }else if(options.position){ // check mouse cursor position (add system to map) newPosition = { x: options.position.x, @@ -417,7 +429,7 @@ define([ system.setSystemRally(1, { poke: Boolean(formData.pokeDesktop) }); - system.markAsChanged(); + MapUtil.markAsChanged(system); // send poke data to server sendPoke(formData, { @@ -637,7 +649,7 @@ define([ * @param system * @returns {string} */ - let getSystemTooltipPlacement = (system) => { + let getSystemTooltipPlacement = system => { let offsetParent = system.parent().offset(); let offsetSystem = system.offset(); @@ -651,7 +663,7 @@ define([ * @param systems * @param callback function */ - let deleteSystems = (map, systems = [], callback = (systems) => {}) => { + let deleteSystems = (map, systems = [], callback = systems => {}) => { let mapContainer = $( map.getContainer() ); let systemIds = systems.map(system => $(system).data('id')); @@ -699,9 +711,11 @@ define([ $(tabContentElement).trigger('pf:removeSystemModules'); } - // remove endpoints and their connections - // do not fire a "connectionDetached" event - map.detachAllConnections(system, {fireEvent: false}); + // remove connections do not fire a "connectionDetached" event + map.deleteConnectionsForElement(system, {fireEvent: false}); + + // unregister from "magnetizer" + Magnetizer.removeElement(system.data('mapid'), system[0]); // destroy tooltip/popover system.toggleSystemTooltip('destroy', {}); @@ -723,7 +737,6 @@ define([ let calculateNewSystemPosition = sourceSystem => { let mapContainer = sourceSystem.parent(); let grid = [MapUtil.config.mapSnapToGridDimension, MapUtil.config.mapSnapToGridDimension]; - let x = 0; let y = 0; @@ -753,12 +766,7 @@ define([ y = currentY + config.newSystemOffset.y; } - let newPosition = { - x: x, - y: y - }; - - return newPosition; + return {x: x, y: y}; }; /** @@ -767,7 +775,7 @@ define([ * @param data * @returns {*} */ - let getHeadInfoElement = (data) => { + let getHeadInfoElement = data => { let headInfo = null; let headInfoLeft = []; let headInfoRight = []; diff --git a/js/app/map/util.js b/js/app/map/util.js index d6894311..9b19fe39 100644 --- a/js/app/map/util.js +++ b/js/app/map/util.js @@ -5,13 +5,17 @@ define([ 'jquery', 'app/init', - 'app/util' -], ($, Init, Util) => { + 'app/util', + 'app/map/scrollbar', + 'app/map/overlay/util' +], ($, Init, Util, Scrollbar, MapOverlayUtil) => { 'use strict'; let config = { mapSnapToGridDimension: 20, // px for grid snapping (grid YxY) defaultLocalJumpRadius: 3, // default search radius (in jumps) for "nearby" pilots + zoomMax: 1.5, + zoomMin: 0.5, // local storage characterLocalStoragePrefix: 'character_', // prefix for character data local storage key @@ -37,32 +41,6 @@ define([ tableCellEllipsis100Class: 'pf-table-cell-100' }; - // map menu options - let mapOptions = { - mapMagnetizer: { - buttonId: Util.config.menuButtonMagnetizerId, - description: 'Magnetizer', - onEnable: 'initMagnetizer', // jQuery extension function - onDisable: 'destroyMagnetizer' // jQuery extension function - }, - mapSnapToGrid : { - buttonId: Util.config.menuButtonGridId, - description: 'Grid snapping', - class: 'mapGridClass' - }, - mapEndpoint : { - buttonId: Util.config.menuButtonEndpointId, - description: 'Endpoint overlay', - onEnable: 'showEndpointOverlays', // jQuery extension function - onDisable: 'hideEndpointOverlays' // jQuery extension function - }, - mapCompact : { - buttonId: Util.config.menuButtonCompactId, - description: 'Compact system layout', - class: 'mapCompactClass' - } - }; - // active jsPlumb instances currently running ===================================================================== let activeInstances = {}; @@ -209,25 +187,28 @@ define([ }; /** - * get system data by mapId and systemid - * @param mapId - * @param systemId - * @returns {boolean} + * get system data from mapData + * @see getSystemData + * @param mapData + * @param value + * @param key + * @returns {any} */ - let getSystemData = (mapId, systemId) => { - let systemData = false; - let mapData = Util.getCurrentMapData(mapId); + let getSystemDataFromMapData = (mapData, value, key = 'id') => { + return mapData ? mapData.data.systems.find(system => system[key] === value) || false : false; + }; - if(mapData){ - for(let j = 0; j < mapData.data.systems.length; j++){ - let systemDataTemp = mapData.data.systems[j]; - if(systemDataTemp.id === systemId){ - systemData = systemDataTemp; - break; - } - } - } - return systemData; + /** + * get system data by mapId system data selector + * -> e.g. value = 2 and key = 'id' + * -> e.g. value = 30002187 and key = 'systemId' => looks for 'Amarr' CCP systemId + * @param mapId + * @param value + * @param key + * @returns {any} + */ + let getSystemData = (mapId, value, key = 'id') => { + return getSystemDataFromMapData(Util.getCurrentMapData(mapId), value, key); }; /** @@ -312,7 +293,7 @@ define([ * @returns {*} */ let filterDefaultTypes = types => { - let defaultTypes = ['', 'default', 'state_active', 'state_process']; + let defaultTypes = ['', 'default', 'info_signature', 'state_active', 'state_process']; return types.diff(defaultTypes); }; @@ -434,7 +415,7 @@ define([ // remove connections from map let removeConnections = connections => { for(let connection of connections){ - connection._jsPlumb.instance.detach(connection, {fireEvent: false}); + connection._jsPlumb.instance.deleteConnection(connection, {fireEvent: false}); } }; @@ -498,19 +479,26 @@ define([ * -> data requires a signature bind to that connection * @param connection * @param connectionData - * @returns {{sourceLabels: Array, targetLabels: Array}} + * @returns {{source: {names: Array, labels: Array}, target: {names: Array, labels: Array}}} */ let getConnectionDataFromSignatures = (connection, connectionData) => { - let signatureTypeNames = { - sourceLabels: [], - targetLabels: [] + let signatureTypeData = { + source: { + names: [], + labels: [] + }, + target: { + names: [], + labels: [] + } }; if( connection && + connectionData && connectionData.signatures // signature data is required... ){ - let SystemSignatures = require('app/ui/module/system_signature'); + let SystemSignatures = require('module/system_signature'); let sourceEndpoint = connection.endpoints[0]; let targetEndpoint = connection.endpoints[1]; @@ -530,11 +518,11 @@ define([ if(signatureData.system.id === sourceId){ // relates to "source" endpoint - tmpSystemType = 'sourceLabels'; + tmpSystemType = 'source'; tmpSystem = sourceSystem; }else if(signatureData.system.id === targetId){ // relates to "target" endpoint - tmpSystemType = 'targetLabels'; + tmpSystemType = 'target'; tmpSystem = targetSystem; } @@ -544,18 +532,19 @@ define([ let availableSigTypeNames = SystemSignatures.getAllSignatureNamesBySystem(tmpSystem, 5); let flattenSigTypeNames = Util.flattenXEditableSelectArray(availableSigTypeNames); - if( flattenSigTypeNames.hasOwnProperty(signatureData.typeId) ){ + if(flattenSigTypeNames.hasOwnProperty(signatureData.typeId)){ let label = flattenSigTypeNames[signatureData.typeId]; - // shorten label, just take the in game name + // shorten label, just take the ingame name label = label.substr(0, label.indexOf(' ')); - signatureTypeNames[tmpSystemType].push(label); + signatureTypeData[tmpSystemType].names.push(signatureData.name); + signatureTypeData[tmpSystemType].labels.push(label); } } } } } - return signatureTypeNames; + return signatureTypeData; }; /** @@ -563,47 +552,53 @@ define([ * -> Coordinates are relative to the Endpoint (not the system!) * -> jsPlumb specific format * @param endpoint - * @param label + * @param labels * @returns {number[]} */ - let getLabelEndpointOverlayLocation = (endpoint, label) => { - let chars = label.length ? label.length : 2; - let xTop = chars === 2 ? +0.05 : chars <= 4 ? -0.75 : 3; - let xLeft = chars === 2 ? -1.10 : chars <= 4 ? -2.75 : 3; - let xRight = chars === 2 ? +1.25 : chars <= 4 ? +1.25 : 3; - let xBottom = chars === 2 ? +0.05 : chars <= 4 ? -0.75 : 3; + let getEndpointOverlaySignatureLocation = (endpoint, labels) => { + let defaultLocation = [0.5, 0.5]; - let yTop = chars === 2 ? -1.10 : chars <= 4 ? -1.75 : 3; + if(endpoint.anchor.getCurrentFace){ + // ContinuousAnchor  + let count = labels.length; + let xLeft = count ? count === 1 ? -1.00 : 3 : -0.5; + let xRight = count ? count === 1 ? +2.20 : 3 : +1.5; - switch(endpoint._continuousAnchorEdge){ - case 'top': return [xTop, yTop]; - case 'left': return [xLeft, 0]; - case 'right': return [xRight, 0]; - case 'bottom': return [xBottom , 1.3]; - default: return [0.0, 0.0]; + switch(endpoint.anchor.getCurrentFace()){ + case 'top': return [0.5, -0.75]; + case 'left': return [xLeft, 0.25]; + case 'right': return [xRight, 0.25]; + case 'bottom': return [0.5 , 1.75]; + default: return defaultLocation; + } + }else{ + // e.g. floating endpoint (dragging) + // -> ContinuousAnchor + return defaultLocation; } }; /** * get overlay HTML for connection endpoints by Label array - * @param label + * @param labels * @returns {string} */ - let getEndpointOverlayContent = label => { + let formatEndpointOverlaySignatureLabel = labels => { + // default K162 in label array, or multiple labels let colorClass = 'txt-color-grayLighter'; + let label = labels.join(', '); - if(label.length > 0){ - // check if multiple labels found => conflict - if( label.includes(', ') ){ - colorClass = 'txt-color-orangeLight'; - }else if( !label.includes('K162') ){ - colorClass = 'txt-color-yellow'; - } - }else{ + if(labels.length === 0){ // endpoint not connected with a signature label = ''; colorClass = 'txt-color-red'; + }else if( + labels.length === 1 && + !labels.includes('K162') + ){ + colorClass = Init.wormholes[labels[0]].class; } + return '' + label + ''; }; @@ -632,64 +627,152 @@ define([ */ let filterMapByScopes = (map, scopes) => { if(map){ - // TODO ^^sometimes map is undefined -> bug - let mapElement = $(map.getContainer()); - let allSystems = mapElement.getSystems(); - let allConnections = map.getAllConnections(); + map.batch(() => { + let mapElement = $(map.getContainer()); + let allSystems = mapElement.getSystems(); + let allConnections = map.getAllConnections(); - if(scopes && scopes.length){ - // filter connections ------------------------------------------------------------------------------------- - let visibleSystems = []; - let visibleConnections = searchConnectionsByScopeAndType(map, scopes); + if(scopes && scopes.length){ + // filter connections ------------------------------------------------------------------------------------- + let visibleSystems = []; + let visibleConnections = searchConnectionsByScopeAndType(map, scopes); - for(let connection of allConnections){ - if(visibleConnections.indexOf(connection) >= 0){ - setConnectionVisible(connection, true); - // source/target system should always be visible -> even if filter scope not matches system type - if(visibleSystems.indexOf(connection.endpoints[0].element) < 0){ - visibleSystems.push(connection.endpoints[0].element); + for(let connection of allConnections){ + if(visibleConnections.indexOf(connection) >= 0){ + setConnectionVisible(connection, true); + // source/target system should always be visible -> even if filter scope not matches system type + if(visibleSystems.indexOf(connection.endpoints[0].element) < 0){ + visibleSystems.push(connection.endpoints[0].element); + } + if(visibleSystems.indexOf(connection.endpoints[1].element) < 0){ + visibleSystems.push(connection.endpoints[1].element); + } + }else{ + setConnectionVisible(connection, false); } - if(visibleSystems.indexOf(connection.endpoints[1].element) < 0){ - visibleSystems.push(connection.endpoints[1].element); - } - }else{ - setConnectionVisible(connection, false); } - } - // filter systems ----------------------------------------------------------------------------------------- - let visibleTypeIds = []; - if(scopes.indexOf('wh') >= 0){ - visibleTypeIds.push(1); - } - if(scopes.indexOf('abyssal') >= 0){ - visibleTypeIds.push(4); - } + // filter systems ----------------------------------------------------------------------------------------- + let visibleTypeIds = []; + if(scopes.indexOf('wh') >= 0){ + visibleTypeIds.push(1); + } + if(scopes.indexOf('abyssal') >= 0){ + visibleTypeIds.push(4); + } - for(let system of allSystems){ - if( - visibleTypeIds.indexOf($(system).data('typeId')) >= 0 || - visibleSystems.indexOf(system) >= 0 - ){ + for(let system of allSystems){ + if( + visibleTypeIds.indexOf($(system).data('typeId')) >= 0 || + visibleSystems.indexOf(system) >= 0 + ){ + setSystemVisible(system, map, true); + }else{ + setSystemVisible(system, map, false); + } + } + + MapOverlayUtil.getMapOverlay(mapElement, 'info').updateOverlayIcon('filter', 'show'); + }else{ + // clear filter + for(let system of allSystems){ setSystemVisible(system, map, true); - }else{ - setSystemVisible(system, map, false); } - } + for(let connection of allConnections){ + setConnectionVisible(connection, true); + } - mapElement.getMapOverlay('info').updateOverlayIcon('filter', 'show'); + MapOverlayUtil.getMapOverlay(mapElement, 'info').updateOverlayIcon('filter', 'hide'); + } + }); + } + }; + + /** + * in/de-crease zoom level + * @param map + * @param zoomAction + * @returns {boolean} + */ + let changeZoom = (map, zoomAction) => { + let zoomChange = false; + let zoom = map.getZoom(); + let zoomStep = 0.1; + if('up' === zoomAction){ + zoom += zoomStep; + }else{ + zoom -= zoomStep; + } + zoom = Math.round(zoom * 10) / 10; + if(zoom >= config.zoomMin && zoom <= config.zoomMax){ + zoomChange = setZoom(map, zoom); + } + return zoomChange; + }; + + /** + * set zoom level for a map + * @param map + * @param zoom + * @returns {boolean} + */ + let setZoom = (map, zoom = 1) => { + let zoomChange = false; + if(zoom !== map.getZoom()){ + // zoom jsPlumb map http://jsplumb.github.io/jsplumb/zooming.html + let transformOrigin = [0, 0]; + let el = map.getContainer(); + let p = ['webkit', 'moz', 'ms', 'o']; + let s = 'scale(' + zoom + ')'; + let oString = (transformOrigin[0] * 100) + '% ' + (transformOrigin[1] * 100) + '%'; + + for(let i = 0; i < p.length; i++){ + el.style[p[i] + 'Transform'] = s; + el.style[p[i] + 'TransformOrigin'] = oString; + } + el.style.transform = s; + el.style.transformOrigin = oString; + + zoomChange = map.setZoom(zoom); + + // adjust mCustomScrollbar -------------------------------------------------------------------------------- + let scaledWidth = el.getBoundingClientRect().width; + let scaledHeight = el.getBoundingClientRect().height; + let mapContainer = $(el); + let mapWidth = mapContainer.outerWidth(); // this is fix (should never change) + let mapHeight = mapContainer.outerHeight(); // this is fix (should never change) + let wrapperWidth = mapContainer.parents('.mCSB_container_wrapper').outerWidth(); // changes on browser resize (map window) + let wrapperHeight = mapContainer.parents('.mCSB_container_wrapper').outerHeight(); // changes on drag resize (map window) + let scrollableWidth = (zoom === 1 || mapWidth !== scaledWidth && scaledWidth > wrapperWidth); + let scrollableHeight = (zoom === 1 || mapHeight !== scaledHeight && scaledHeight > wrapperHeight); + + mapContainer.parents('.mCSB_container').css({ + 'width': scrollableWidth ? scaledWidth + 'px' : (wrapperWidth - 50) + 'px', + 'height': scrollableHeight ? scaledHeight + 'px' : (wrapperHeight) + 'px', + }); + + let mapWrapperElement = mapContainer.closest('.mCustomScrollbar'); + if(scrollableWidth && scrollableHeight){ + mapWrapperElement.mCustomScrollbar('update'); }else{ - // clear filter - for(let system of allSystems){ - setSystemVisible(system, map, true); - } - for(let connection of allConnections){ - setConnectionVisible(connection, true); - } - - mapElement.getMapOverlay('info').updateOverlayIcon('filter', 'hide'); + mapWrapperElement.mCustomScrollbar('scrollTo', '#' + mapContainer.attr('id'), { + scrollInertia: 0, + scrollEasing: 'linear', + timeout: 0, + moveDragger: false + }); } } + + return zoomChange; + }; + + /** + * toggles editable input form for system rename (set alias) + * @param system + */ + let toggleSystemAliasEditable = system => { + system.find('.editable').editable('toggle'); }; /** @@ -744,20 +827,113 @@ define([ system.toggleClass(config.systemHiddenClass, !visible); }; + /** + * add/remove connection type to connection that was previous registered by registerConnectionTypes() + * -> this method is a wrapper for addType()/removeType() + * with the addition of respecting active Arrow overlay direction + * @param action + * @param connection + * @param types + * @param params + * @param doNotRepaint + */ + let changeConnectionTypes = (action, connection, types = [], params = [], doNotRepaint = false) => { + + if(connection && types.length){ + // check for active Arrow overlay + let overlayArrow, overlayArrowParams; + if( + !types.includes('info_signature') && + connection.hasType('info_signature') + ){ + overlayArrow = connection.getOverlay(MapOverlayUtil.config.connectionOverlayArrowId); + if(overlayArrow){ + overlayArrowParams = { + direction: overlayArrow.direction, + foldback: overlayArrow.foldback, + }; + } + } + + for(let i = 0; i < types.length; i++){ + // change the new type + connection[action](types[i], typeof params[i] === 'object' ? params[i] : {}, doNotRepaint); + } + + // change Arrow overlay data back to initial direction + if( + overlayArrow && + ( + overlayArrow.direction !== overlayArrowParams.direction || + overlayArrow.foldback !== overlayArrowParams.foldback + ) + ){ + overlayArrow.updateFrom(overlayArrowParams); + if(!doNotRepaint){ + connection.repaint(); + } + } + } + }; + + /** + * add connection type to connection that was previous registered by registerConnectionTypes() + * @param connection + * @param type + * @param params + * @param doNotRepaint + */ + let addConnectionType = (connection, type, params, doNotRepaint = false) => { + addConnectionTypes(connection, [type], typeof params === 'object' ? [params] : [], doNotRepaint); + }; + + let addConnectionTypes = (connection, types = [], params = [], doNotRepaint = false) => { + if(connection){ + changeConnectionTypes('addType', connection, types.diff(connection.getType()), params, doNotRepaint); + } + }; + + /** + * remove connection type to connection that was previous registered by registerConnectionTypes() + * @param connection + * @param type + * @param params + * @param doNotRepaint + */ + let removeConnectionType = (connection, type, params, doNotRepaint = false) => { + removeConnectionTypes(connection, [type], typeof params === 'object' ? [params] : [], doNotRepaint); + }; + + let removeConnectionTypes = (connection, types = [], params = [], doNotRepaint = false) => { + if(connection){ + changeConnectionTypes('removeType', connection, types.intersect(connection.getType()), params, doNotRepaint); + } + }; + + let toggleConnectionType = (connection, type, params, doNotRepaint = false) => { + changeConnectionTypes('toggleType', connection, [type], typeof params === 'object' ? [params] : [], doNotRepaint); + }; + /** * mark a connection as "active" * @param map * @param connections */ let setConnectionsActive = (map, connections) => { - // set all inactive - for(let connection of getConnectionsByType(map, 'state_active')){ - connection.removeType('state_active'); - } + map.batch(() => { + // set all inactive + for(let connection of getConnectionsByType(map, 'state_active')){ + if(!connections.includes(connection)){ + removeConnectionType(connection, 'state_active'); + } + } - for(let connection of connections){ - connection.addType('state_active'); - } + for(let connection of connections){ + if(!connection.hasType('state_active')){ + addConnectionType(connection, 'state_active'); + } + } + }); }; /** @@ -766,8 +942,11 @@ define([ * @param visible */ let setConnectionVisible = (connection, visible) => { - for(let endpoint of connection.endpoints){ - endpoint.setVisible(visible); + if(connection.isVisible() !== visible){ + connection.setVisible(visible, true); + for(let endpoint of connection.endpoints){ + endpoint.setVisible(visible, true); + } } }; @@ -797,15 +976,18 @@ define([ let toggleConnectionActive = (map, connections) => { let selectedConnections = []; let deselectedConnections = []; - for(let connection of connections){ - if(connection.hasType('state_active')){ - connection.removeType('state_active'); - deselectedConnections.push(connection); - }else{ - connection.addType('state_active'); - selectedConnections.push(connection); + map.batch(() => { + for(let connection of connections){ + if(connection.hasType('state_active')){ + removeConnectionType(connection, 'state_active'); + deselectedConnections.push(connection); + }else{ + addConnectionType(connection, 'state_active'); + selectedConnections.push(connection); + } } - } + }); + updateConnectionInfo(map, selectedConnections, deselectedConnections); }; @@ -960,10 +1142,10 @@ define([ * @param types * @returns {string[]} */ - let getConnectionFakeClassesByTypes = (types) => { + let getConnectionFakeClassesByTypes = types => { let connectionClasses = ['pf-fake-connection']; for(let i = 0; i < types.length; i++){ - connectionClasses.push(getConnectionInfo( types[i], 'cssClass')); + connectionClasses.push(getConnectionInfo(types[i], 'cssClass')); } return connectionClasses; }; @@ -971,9 +1153,9 @@ define([ /** * get all direct connections between two given systems * @param map - * @param {JQuery} systemA - * @param {JQuery} systemB - * @returns {Array} + * @param systemA + * @param systemB + * @returns {*[]} */ let checkForConnection = (map, systemA, systemB) => { let connections = []; @@ -989,21 +1171,17 @@ define([ * @param {string} scope * @returns {string} */ - let getDefaultConnectionTypeByScope = (scope) => { + let getDefaultConnectionTypeByScope = scope => { let type = ''; switch(scope){ case 'wh': - type = 'wh_fresh'; - break; + type = 'wh_fresh'; break; case 'jumpbridge': - type = 'jumpbridge'; - break; + type = 'jumpbridge'; break; case'stargate': - type = 'stargate'; - break; + type = 'stargate'; break; case'abyssal': - type = 'abyssal'; - break; + type = 'abyssal'; break; default: console.error('Connection scope "' + scope + '" unknown!'); } @@ -1012,43 +1190,56 @@ define([ }; /** - * set/change connection status of a wormhole - * @param {Object} connection - jsPlumb object - * @param {string} status + * get all available connection types for "mass status" + * @returns {string[]} */ - let setConnectionWHStatus = (connection, status) => { - if( - status === 'wh_fresh' && - connection.hasType('wh_fresh') !== true - ){ - connection.removeType('wh_reduced'); - connection.removeType('wh_critical'); - connection.addType('wh_fresh'); - }else if( - status === 'wh_reduced' && - connection.hasType('wh_reduced') !== true - ){ - connection.removeType('wh_fresh'); - connection.removeType('wh_critical'); - connection.addType('wh_reduced'); - }else if( - status === 'wh_critical' && - connection.hasType('wh_critical') !== true - ){ - connection.removeType('wh_fresh'); - connection.removeType('wh_reduced'); - connection.addType('wh_critical'); - }else if( - status === 'wh_eol' && - connection.hasType('wh_eol') !== true - ){ - connection.addType('wh_eol'); - }else if( - status === 'wh_eol' && - connection.hasType('wh_eol') !== true - ){ - connection.addType('wh_eol'); - } + let allConnectionMassStatusTypes = () => { + return ['wh_fresh', 'wh_reduced', 'wh_critical']; + }; + + /** + * get all available connection types for "jump mass size" + * @returns {string[]} + */ + let allConnectionJumpMassTypes = () => { + return ['wh_jump_mass_s', 'wh_jump_mass_m', 'wh_jump_mass_l', 'wh_jump_mass_xl']; + }; + + /** + * set/change/remove connection mass status of connection + * -> statusType == undefined will remove (all) existing mass status types + * @param connection + * @param statusType + */ + let setConnectionMassStatusType = (connection, statusType) => { + setUniqueConnectionType(connection, statusType, allConnectionMassStatusTypes()); + + }; + + /** + * set/change/remove connection jump mass of a connection + * -> massType == undefined will remove (all) existing jump mass types + * @param connection + * @param massType + */ + let setConnectionJumpMassType = (connection, massType) => { + setUniqueConnectionType(connection, massType, allConnectionJumpMassTypes()); + }; + + /** + * set/change/remove connection type + * -> type == undefined will remove (all) existing provided types + * @param connection + * @param type + * @param types + */ + let setUniqueConnectionType = (connection, type, types) => { + type = types.includes(type) ? [type] : []; + + connection._jsPlumb.instance.batch(() => { + removeConnectionTypes(connection, types.diff(type)); + addConnectionTypes(connection, type); + }); }; /** @@ -1163,12 +1354,12 @@ define([ let visualizeMapExecutor = (resolve, reject) => { // start map update counter -> prevent map updates during animations - mapElement.getMapOverlay('timer').startMapUpdateCounter(); + MapOverlayUtil.getMapOverlay(mapElement, 'timer').startMapUpdateCounter(); let systemElements = mapElement.find('.' + config.systemClass); - let endpointElements = mapElement.find('.jsplumb-endpoint:visible'); - let connectorElements = mapElement.find('.jsplumb-connector:visible'); - let overlayElements = mapElement.find('.jsplumb-overlay:visible, .tooltip'); + let endpointElements = mapElement.find('.jtk-endpoint:visible'); + let connectorElements = mapElement.find('.jtk-connector:visible'); + let overlayElements = mapElement.find('.jtk-overlay:visible, .tooltip'); let hideElements = (elements) => { if(elements.length > 0){ @@ -1257,8 +1448,9 @@ define([ }; /** - * set default map Options ( - * -> HINT: This function triggers Events! Promise is resolved before trigger completed + * Set default map options (from right menu) + * This function is called only ONCE per map after create! + * -> HINT: This function triggers events! Promise is resolved before trigger callback finishes * @param mapElement * @param mapConfig * @returns {Promise} @@ -1272,27 +1464,29 @@ define([ }); // init compact system layout --------------------------------------------------------------------- - mapElement.triggerMenuEvent('MapOption', { + Util.triggerMenuAction(mapElement, 'MapOption', { option: 'mapCompact', toggle: false }); // init magnetizer -------------------------------------------------------------------------------- - mapElement.triggerMenuEvent('MapOption', { + Util.triggerMenuAction(mapElement, 'MapOption', { option: 'mapMagnetizer', toggle: false }); // init grid snap --------------------------------------------------------------------------------- - mapElement.triggerMenuEvent('MapOption', { + Util.triggerMenuAction(mapElement, 'MapOption', { option: 'mapSnapToGrid', toggle: false }); // init endpoint overlay -------------------------------------------------------------------------- - mapElement.triggerMenuEvent('MapOption', { - option: 'mapEndpoint', - toggle: false + Util.triggerMenuAction(mapElement, 'MapOption', { + option: 'mapSignatureOverlays', + toggle: false, + skipOnEnable: true, // skip callback -> Otherwise it would run 2 times on map create + skipOnDisable: true // skip callback -> Otherwise it would run 2 times on map create }); resolve({ @@ -1321,30 +1515,61 @@ define([ /** * scroll map to default (stored) x/y coordinates - * @param mapElement + * @param map * @returns {Promise} */ - let scrollToDefaultPosition = (mapElement) => { + let scrollToDefaultPosition = map => { - let scrollToDefaultPositionExecutor = (resolve, reject) => { - let mapWrapper = mapElement.parents('.' + config.mapWrapperClass); + let scrollToDefaultPositionExecutor = resolve => { + let payload = { + action: 'scrollToDefaultPosition', + data: false + }; - // auto scroll map to previous stored position + // no map scroll on zoomed maps -> scrollbar offset on zoomed maps does not work properly + // -> implementation would be difficult... + if(map.getZoom() === 1){ + let mapElement = $(map.getContainer()); + let promiseStore = getLocaleData('map', mapElement.data('id')); + promiseStore.then(data => { + if(data && data.scrollOffset){ + let mapWrapper = mapElement.parents('.' + config.mapWrapperClass); + Scrollbar.scrollToPosition(mapWrapper, [data.scrollOffset.y, data.scrollOffset.x]); + } + + resolve(payload); + }); + }else{ + resolve(payload); + } + }; + + return new Promise(scrollToDefaultPositionExecutor); + }; + + /** + * zoom map to default (stored) scale() + * @param map + * @returns {Promise} + */ + let zoomToDefaultScale = map => { + + let zoomToDefaultScaleExecutor = resolve => { + let mapElement = $(map.getContainer()); let promiseStore = getLocaleData('map', mapElement.data('id')); promiseStore.then(data => { - // This code runs once the value has been loaded from offline storage - if(data && data.scrollOffset){ - mapWrapper.scrollToPosition([data.scrollOffset.y, data.scrollOffset.x]); + if(data && data.mapZoom){ + setZoom(map, data.mapZoom); } resolve({ - action: 'scrollToDefaultPosition', + action: 'zoomToDefaultScale', data: false }); }); }; - return new Promise(scrollToDefaultPositionExecutor); + return new Promise(zoomToDefaultScaleExecutor); }; /** @@ -1397,7 +1622,7 @@ define([ if(rallyUpdated !== rally){ // rally status changed if( !options.hideCounter ){ - system.getMapOverlay('timer').startMapUpdateCounter(); + MapOverlayUtil.getMapOverlay(system, 'timer').startMapUpdateCounter(); } let rallyClass = getInfoForSystem('rally', 'class'); @@ -1652,13 +1877,12 @@ define([ let title = tooltipData.name; + if(tooltipData.size){ + title += '  ' + tooltipData.size.label + ''; + } + if(tooltipData.security){ // K162 has no security - - if(!tooltipData.class){ - tooltipData.class = Util.getSecurityClassForSystem(tooltipData.security); - } - title += '' + tooltipData.security + ''; } @@ -1697,6 +1921,38 @@ define([ }); }; + /** + * + * @param container any parent element that holds the event + * @param selector element that bubbles up hover + * @param options tooltip options + */ + let initWormholeInfoTooltip = (container, selector, options = {}) => { + let defaultOptions = { + trigger: 'manual', + placement: 'top', + smaller: false, + show: true + }; + + options = Object.assign({}, defaultOptions, options); + + container.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, options); + } + }, + out: function(e){ + $(this).destroyPopover(); + }, + selector: selector + }); + }; + /** * get systemId string (selector * @param mapId @@ -1788,7 +2044,6 @@ define([ return { config: config, - mapOptions: mapOptions, setMapInstance: setMapInstance, getMapInstance: getMapInstance, existsMapInstance: existsMapInstance, @@ -1799,17 +2054,22 @@ define([ getMapIcons: getMapIcons, getInfoForMap: getInfoForMap, getInfoForSystem: getInfoForSystem, + getSystemDataFromMapData: getSystemDataFromMapData, getSystemData: getSystemData, getSystemTypeInfo: getSystemTypeInfo, getEffectInfoForSystem: getEffectInfoForSystem, markAsChanged: markAsChanged, hasChanged: hasChanged, toggleSystemsSelect: toggleSystemsSelect, + addConnectionTypes: addConnectionTypes, + removeConnectionTypes: removeConnectionTypes, + toggleConnectionType: toggleConnectionType, toggleConnectionActive: toggleConnectionActive, setSystemActive: setSystemActive, showSystemInfo: showSystemInfo, showConnectionInfo: showConnectionInfo, showFindRouteDialog: showFindRouteDialog, + filterDefaultTypes: filterDefaultTypes, getEndpointLabel: getEndpointLabel, getConnectionsByType: getConnectionsByType, getEndpointsDataByConnection: getEndpointsDataByConnection, @@ -1820,16 +2080,22 @@ define([ getConnectionFakeClassesByTypes: getConnectionFakeClassesByTypes, checkForConnection: checkForConnection, getDefaultConnectionTypeByScope: getDefaultConnectionTypeByScope, - setConnectionWHStatus: setConnectionWHStatus, + allConnectionMassStatusTypes: allConnectionMassStatusTypes, + allConnectionJumpMassTypes: allConnectionJumpMassTypes, + setConnectionMassStatusType: setConnectionMassStatusType, + setConnectionJumpMassType: setConnectionJumpMassType, getScopeInfoForConnection: getScopeInfoForConnection, getDataByConnections: getDataByConnections, deleteConnections: deleteConnections, getConnectionDataFromSignatures: getConnectionDataFromSignatures, - getLabelEndpointOverlayLocation: getLabelEndpointOverlayLocation, - getEndpointOverlayContent: getEndpointOverlayContent, + getEndpointOverlaySignatureLocation: getEndpointOverlaySignatureLocation, + formatEndpointOverlaySignatureLabel: formatEndpointOverlaySignatureLabel, getTabContentElementByMapElement: getTabContentElementByMapElement, hasActiveConnection: hasActiveConnection, filterMapByScopes: filterMapByScopes, + changeZoom: changeZoom, + setZoom: setZoom, + toggleSystemAliasEditable: toggleSystemAliasEditable, storeLocaleCharacterData: storeLocaleCharacterData, getLocaleData: getLocaleData, storeLocalData: storeLocalData, @@ -1838,6 +2104,8 @@ define([ setMapDefaultOptions: setMapDefaultOptions, getSystemPosition: getSystemPosition, scrollToDefaultPosition: scrollToDefaultPosition, + zoomToDefaultScale: zoomToDefaultScale, + initWormholeInfoTooltip: initWormholeInfoTooltip, getSystemId: getSystemId, checkRight: checkRight, getMapDeeplinkUrl: getMapDeeplinkUrl diff --git a/js/app/map/worker.js b/js/app/map/worker.js index 7702c686..b65d910d 100644 --- a/js/app/map/worker.js +++ b/js/app/map/worker.js @@ -4,13 +4,9 @@ define([ 'app/util' -], function(Util){ +], (Util) => { 'use strict'; - let config = { - - }; - let sharedWorker = null; let MsgWorker = null; let characterId = null; @@ -29,17 +25,13 @@ define([ * get SharedWorker Script path * @returns {string} */ - let getWorkerScript = () => { - return '/public/js/' + Util.getVersion() + '/app/worker/map.js'; - }; + let getWorkerScript = () => '/public/js/' + Util.getVersion() + '/app/worker/map.js'; /** * get path to message object * @returns {string} */ - let getMessageWorkerObjectPath = () => { - return '/public/js/' + Util.getVersion() + '/app/worker/message.js'; - }; + let getMessageWorkerObjectPath = () => '/public/js/' + Util.getVersion() + '/app/worker/message.js'; /** * init (connect) WebSocket within SharedWorker @@ -51,14 +43,14 @@ define([ characterId: characterId, }); - sharedWorker.port.postMessage(MsgWorkerInit); + sendMessage(MsgWorkerInit); }; /** - * init (start/connect) to "SharedWorker" + * init (start/connect) to "SharedWorker" thread * -> set worker events */ - let init = (config) => { + let init = config => { // set characterId that is connected with this SharedWorker PORT characterId = parseInt(config.characterId); @@ -67,9 +59,9 @@ define([ MsgWorker = window.MsgWorker; // start/connect to "SharedWorker" - sharedWorker = new SharedWorker( getWorkerScript(), getMessageWorkerObjectPath() ); + sharedWorker = new SharedWorker(getWorkerScript(), getMessageWorkerObjectPath()); - sharedWorker.port.addEventListener('message', (e) => { + sharedWorker.port.addEventListener('message', e => { let MsgWorkerMessage = e.data; Object.setPrototypeOf(MsgWorkerMessage, MsgWorker.prototype); @@ -90,7 +82,7 @@ define([ } }, false); - sharedWorker.onerror = (e) => { + sharedWorker.onerror = e => { // could not connect to SharedWorker script -> send error back let MsgWorkerError = new MsgWorker('sw:error'); MsgWorkerError.meta({ @@ -111,17 +103,52 @@ define([ }); }; + /** + * send data to "SharedWorker" thread + * @param task + * @param data + */ let send = (task, data) => { let MsgWorkerSend = new MsgWorker('ws:send'); MsgWorkerSend.task(task); MsgWorkerSend.data(data); - sharedWorker.port.postMessage(MsgWorkerSend); + sendMessage(MsgWorkerSend); + }; + + /** + * send close port task to "SharedWorker" thread + * -> this removes the port from its port collection and closes it + */ + let close = () => { + // check if MsgWorker is available (SharedWorker was initialized) + if(MsgWorker){ + let MsgWorkerClose = new MsgWorker('sw:closePort'); + MsgWorkerClose.task('unsubscribe'); + sendMessage(MsgWorkerClose); + } + }; + + /** + * + * @param {window.MsgWorker} MsgWorkerSend + */ + let sendMessage = MsgWorkerSend => { + if(sharedWorker instanceof SharedWorker){ + if(MsgWorkerSend instanceof window.MsgWorker){ + sharedWorker.port.postMessage(MsgWorkerSend); + }else{ + console.error('MsgWorkerSend must be instance of window.MsgWorker'); + } + }else{ + console.error('SharedWorker thread not found'); + } }; return { getWebSocketURL: getWebSocketURL, init: init, - send: send + send: send, + close: close }; }); \ No newline at end of file diff --git a/js/app/mappage.js b/js/app/mappage.js index 8fe8ae50..8c34ab2e 100644 --- a/js/app/mappage.js +++ b/js/app/mappage.js @@ -6,14 +6,13 @@ define([ 'jquery', 'app/init', 'app/util', - 'app/render', 'app/logging', 'app/page', 'app/map/worker', 'app/module_map', 'app/key', 'app/ui/form_element' -], ($, Init, Util, Render, Logging, Page, MapWorker, ModuleMap) => { +], ($, Init, Util, Logging, Page, MapWorker, ModuleMap) => { 'use strict'; @@ -39,7 +38,7 @@ define([ Util.initDefaultEditableConfig(); // load page - $('body').loadPageStructure().setGlobalShortcuts(); + Page.loadPageStructure(); // show app information in browser console Util.showVersionInfo(); @@ -121,6 +120,25 @@ define([ */ let initData = () => { + /** + * add wormhole size data for each wormhole + * @param wormholes + * @returns {*} + */ + let addWormholeSizeData = wormholes => { + for(let [wormholeName, wormholeData] of Object.entries(wormholes)){ + wormholeData.class = Util.getSecurityClassForSystem(wormholeData.security); + + for(let [sizeName, sizeData] of Object.entries(Init.wormholeSizes)){ + if(wormholeData.massIndividual >= sizeData.jumpMassMin){ + wormholeData.size = sizeData; + break; + } + } + } + return wormholes; + }; + let initDataExecutor = (resolve, reject) => { $.getJSON(Init.path.initData).done(response => { if( response.error.length > 0 ){ @@ -139,7 +157,7 @@ define([ Init.connectionScopes = response.connectionScopes; Init.systemStatus = response.systemStatus; Init.systemType = response.systemType; - Init.wormholes = response.wormholes; + Init.wormholes = addWormholeSizeData(response.wormholes); Init.characterStatus = response.characterStatus; Init.routes = response.routes; Init.url = response.url; @@ -202,7 +220,7 @@ define([ * @param payload * @returns {Promise} */ - let initMapModule = (payload) => { + let initMapModule = payload => { let initMapModuleExecutor = (resolve, reject) => { // init browser tab change observer, Once the timers are available @@ -229,10 +247,10 @@ define([ * @param payloadMapAccessData * @returns {Promise} */ - let initMapWorker = (payloadMapAccessData) => { + let initMapWorker = payloadMapAccessData => { let initMapWorkerExecutor = (resolve, reject) => { - let getPayload = (command) => { + let getPayload = command => { return { action: 'initMapWorker', data: { @@ -251,7 +269,7 @@ define([ // init SharedWorker for maps MapWorker.init({ - characterId: response.data.id, + characterId: response.data.id, callbacks: { onInit: (MsgWorkerMessage) => { Util.setSyncStatus(MsgWorkerMessage.command); @@ -398,7 +416,7 @@ define([ data.error.length > 0 ){ // any error in the main trigger functions result in a user log-off - $(document).trigger('pf:menuLogout'); + Util.triggerMenuAction(document, 'Logout'); }else{ $(document).setProgramStatus('online'); @@ -474,13 +492,13 @@ define([ data.error.length > 0 ){ // any error in the main trigger functions result in a user log-off - $(document).trigger('pf:menuLogout'); + Util.triggerMenuAction(document, 'Logout'); }else{ $(document).setProgramStatus('online'); if(data.userData !== undefined){ // store current user data global (cache) - let userData = Util.setCurrentUserData(data.userData); + Util.setCurrentUserData(data.userData); // update system info panels if(data.system){ @@ -530,11 +548,42 @@ define([ // initial start of the map update function triggerMapUpdatePing(true); + /** + * handles "final" map update request before window.unload event + * -> navigator.sendBeacon browser support required + * ajax would not work here, because browsers might cancel the request! + * @param mapModule + */ + let mapUpdateUnload = mapModule => { + // get updated map data + let mapData = ModuleMap.getMapModuleDataForUpdate(mapModule); + + if(mapData.length){ + let fd = new FormData(); + fd.set('mapData', JSON.stringify(mapData)); + navigator.sendBeacon(Init.path.updateUnloadData, fd); + + console.info('Map update request send by: %O', navigator.sendBeacon); + } + }; + // Send map update request on tab close/reload, in order to save map changes that // haven´t been saved through default update trigger window.addEventListener('beforeunload', function(e){ + // close "SharedWorker" connection + MapWorker.close(); + + // clear periodic update timeouts + // -> this function will handle the final map update request + clearUpdateTimeouts(); + // save unsaved map changes ... - triggerMapUpdatePing(); + if(navigator.sendBeacon){ + mapUpdateUnload(mapModule); + }else{ + // fallback if sendBeacon() is not supported by browser + triggerMapUpdatePing(); + } // check if character should be switched on reload or current character should be loaded afterwards let characterSwitch = Boolean( $('body').data('characterSwitch') ); @@ -547,7 +596,7 @@ define([ // IMPORTANT, return false in order to not "abort" ajax request in background! return false; - }); + }, false); }; diff --git a/js/app/module_map.js b/js/app/module_map.js index df3d1e3c..2863bf97 100644 --- a/js/app/module_map.js +++ b/js/app/module_map.js @@ -5,13 +5,13 @@ define([ 'app/map/map', 'app/map/util', 'sortable', - 'app/ui/module/system_info', - 'app/ui/module/system_graph', - 'app/ui/module/system_signature', - 'app/ui/module/system_route', - 'app/ui/module/system_intel', - 'app/ui/module/system_killboard', - 'app/ui/module/connection_info', + 'module/system_info', + 'module/system_graph', + 'module/system_signature', + 'module/system_route', + 'module/system_intel', + 'module/system_killboard', + 'module/connection_info', 'app/counter' ], ( $, @@ -54,18 +54,16 @@ define([ moduleClass: 'pf-module', // class for a module moduleSpacerClass: 'pf-module-spacer', // class for "spacer" module (preserves height during hide/show animation) moduleClosedClass: 'pf-module-closed' // class for a closed module - }; let mapTabChangeBlocked = false; // flag for preventing map tab switch /** * get all maps for a maps module - * @returns {*} + * @param mapModule + * @returns {jQuery} */ - $.fn.getMaps = function(){ - return $(this).find('.' + config.mapClass); - }; + let getMaps = mapModule => $(mapModule).find('.' + config.mapClass); /** * get the current active mapElement @@ -748,7 +746,7 @@ define([ MapUtil.storeLocaleCharacterData('defaultMapId', mapId); }else{ // add new Tab selected - $(document).trigger('pf:menuShowMapSettings', {tab: 'new'}); + Util.triggerMenuAction(document, 'ShowMapSettings', {tab: 'new'}); e.preventDefault(); } }); @@ -1217,7 +1215,7 @@ define([ clearMapModule(mapModule) .then(payload => { // no map data available -> show "new map" dialog - $(document).trigger('pf:menuShowMapSettings', {tab: 'new'}); + Util.triggerMenuAction(document, 'ShowMapSettings', {tab: 'new'}); }) .then(payload => resolve()); }else{ @@ -1341,16 +1339,17 @@ define([ * collect all data (systems/connections) for export/save from each active map in the map module * if no change detected -> do not attach map data to return array * @param mapModule + * @param filter * @returns {Array} */ - let getMapModuleDataForUpdate = mapModule => { + let getMapModuleDataForUpdate = (mapModule, filter = ['hasId', 'hasChanged']) => { // get all active map elements for module - let mapElements = mapModule.getMaps(); + let mapElements = getMaps(mapModule); let data = []; for(let i = 0; i < mapElements.length; i++){ // get all changed (system / connection) data from this map - let mapData = Map.getMapDataForSync($(mapElements[i]), ['hasId', 'hasChanged']); + let mapData = Map.getMapDataForSync($(mapElements[i]), filter); if(mapData !== false){ if( mapData.data.systems.length > 0 || diff --git a/js/app/page.js b/js/app/page.js index bd21cf42..8d752acb 100644 --- a/js/app/page.js +++ b/js/app/page.js @@ -10,9 +10,10 @@ define([ 'mustache', 'app/map/util', 'app/map/contextmenu', + 'slidebars', 'text!img/logo.svg!strip', - 'text!templates/modules/header.html', - 'text!templates/modules/footer.html', + 'text!templates/layout/header_map.html', + 'text!templates/layout/footer_map.html', 'dialog/notification', 'dialog/stats', 'dialog/map_info', @@ -25,21 +26,16 @@ define([ 'dialog/delete_account', 'dialog/credit', 'xEditable', - 'slidebars', 'app/module_map' -], ($, Init, Util, Logging, Mustache, MapUtil, MapContextMenu, TplLogo, TplHead, TplFooter) => { +], ($, Init, Util, Logging, Mustache, MapUtil, MapContextMenu, SlideBars, TplLogo, TplHead, TplFooter) => { 'use strict'; let config = { - // page structure slidebars-menu classes - pageId: 'sb-site', - pageSlidebarClass: 'sb-slidebar', - pageSlidebarLeftClass: 'sb-left', // class for left menu - pageSlidebarRightClass: 'sb-right', // class for right menu - pageSlideLeftWidth: '150px', // slide distance left menu - pageSlideRightWidth: '150px', // slide distance right menu - fullScreenClass: 'pf-fullscreen', // class for the "full screen" element + // page structure and slide menus + pageMenuClass: 'pf-menu', + pageMenuLeftClass: 'pf-menu-left', // class for left menu + pageMenuRightClass: 'pf-menu-right', // class for right menu // page structure pageClass: 'pf-site', @@ -52,17 +48,17 @@ define([ headUserCharacterClass: 'pf-head-user-character', // class for "user settings" link userCharacterImageClass: 'pf-head-user-character-image', // class for "current user image" - headUserShipClass: 'pf-head-user-ship', // class for "user settings" link - userShipImageClass: 'pf-head-user-ship-image', // class for "current user ship image" - headActiveUserClass: 'pf-head-active-user', // class for "active user" link + headActiveUsersClass: 'pf-head-active-users', // class for "active users" link headProgramStatusClass: 'pf-head-program-status', // class for "program status" notification + headMaxLocationHistoryBreadcrumbs: 3, // max breadcrumb count for character log history + // footer footerLicenceLinkClass: 'pf-footer-licence', // class for "licence" link + footerClockClass: 'pf-footer-clock', // class for EVE-Time clock // menu menuHeadMenuLogoClass: 'pf-head-menu-logo', // class for main menu logo - menuClockClass: 'pf-menu-clock', // class for EVE-Time clock // helper element dynamicElementWrapperId: 'pf-dialog-wrapper', // class for container element that holds hidden "context menus" @@ -72,71 +68,838 @@ define([ systemIntelModuleClass: 'pf-system-intel-module', // module wrapper (intel) }; + let menuBtnTypes = { + 'info': {class: 'list-group-item-info'}, + 'danger': {class: 'list-group-item-danger'}, + 'warning': {class: 'list-group-item-warning'} + }; + let programStatusCounter = 0; // current count down in s until next status change is possible let programStatusInterval = false; // interval timer until next status change is possible + /** + * set an DOM element to fullscreen mode + * @ see https://developer.mozilla.org/docs/Web/API/Fullscreen_API + * @param element + */ + let toggleFullScreen = element => { + if( + document.fullscreenEnabled && + element.requestFullscreen + ){ + let fullScreenButton = $('#' + Util.config.menuButtonFullScreenId); + + if(!document.fullscreenElement){ + element.requestFullscreen().then(() => { + fullScreenButton.toggleClass('active', true); + // close open menu + Util.triggerMenuAction(document, 'Close'); + }); + }else{ + if(document.exitFullscreen){ + document.exitFullscreen().then(() => { + fullScreenButton.toggleClass('active', false); + }); + } + } + } + }; /** - * load main page structure elements and navigation container into body - * @returns {*|jQuery|HTMLElement} + * init tooltips in header navbar + * @param element */ - $.fn.loadPageStructure = function(){ - return this.each((i, body) => { - body = $(body); - - // menu left - body.prepend( - $('
', { - class: [config.pageSlidebarClass, config.pageSlidebarLeftClass, 'sb-style-push', 'sb-width-custom'].join(' ') - }).attr('data-sb-width', config.pageSlideLeftWidth) - ); - - // menu right - body.prepend( - $('
', { - class: [config.pageSlidebarClass, config.pageSlidebarRightClass, 'sb-style-push', 'sb-width-custom'].join(' ') - }).attr('data-sb-width', config.pageSlideRightWidth) - ); - - // main page - body.prepend( - $('
', { - id: config.pageId, - class: config.pageClass - }).append( - Util.getMapModule(), - $('
', { - id: config.dynamicElementWrapperId - }) - ) - ); - - // load footer - $('.' + config.pageClass).loadHeader().loadFooter(); - - // load left menu - $('.' + config.pageSlidebarLeftClass).loadLeftMenu(); - - // load right menu - $('.' + config.pageSlidebarRightClass).loadRightMenu(); - - // set page observer for global events - setPageObserver(); + let initHeaderTooltips = element => { + element.find('[title]').tooltip({ + placement: 'bottom', + delay: { + show: 500, + hide: 0 + } }); }; /** - * set global shortcuts to element + * load main page structure elements and navigation container into body + * @returns {*|w.fn.init|jQuery|HTMLElement} */ - $.fn.setGlobalShortcuts = function(){ - return this.each((i, body) => { - body = $(body); + let loadPageStructure = () => { - body.watchKey('tabReload', (body) => { + let executor = resolve => { + let body = $('body'); + + let pageElement = $('
', { + class: config.pageClass + }).attr('canvas', 'container').append( + Util.getMapModule(), + $('
', { + id: config.dynamicElementWrapperId + }) + ); + + let pageMenuLeftElement = $('
', { + class: [config.pageMenuClass, config.pageMenuLeftClass].join(' ') + }).attr('off-canvas', [config.pageMenuLeftClass, 'left', 'push'].join(' ')); + + let pageMenuRightElement = $('
', { + class: [config.pageMenuClass, config.pageMenuRightClass].join(' ') + }).attr('off-canvas', [config.pageMenuRightClass, 'right', 'push'].join(' ')); + + body.prepend(pageElement, pageMenuLeftElement, pageMenuRightElement); + + Promise.all([ + loadHeader(pageElement), + loadFooter(pageElement), + loadLeftMenu(pageMenuLeftElement), + loadRightMenu(pageMenuRightElement), + ]).then(payload => Promise.all([ + setMenuObserver(payload[2].data), + setMenuObserver(payload[3].data), + setDocumentObserver(), + setBodyObserver(), + setGlobalShortcuts() + ])).then(() => resolve({ + action: 'loadPageStructure', + data: {} + })); + }; + + return new Promise(executor); + }; + + /** + * build main menu from mapConfig array + * @param menuConfig + * @returns {*|w.fn.init|jQuery|HTMLElement} + */ + let getMenu = menuConfig => { + let menu = $('
', {class: 'list-group'}); + + for(let itemConfig of menuConfig){ + if(!itemConfig) continue; // skip "null" items -> if item is not available + let item; + + switch(itemConfig.type){ + case 'button': + let className = Util.getObjVal(menuBtnTypes, itemConfig.btnType + '.class') || ''; + className += itemConfig.class || ''; + + item = $('', { + id: itemConfig.id || undefined, + class: 'list-group-item ' + className, + href: itemConfig.href || undefined, + html: '  ' + itemConfig.label + }); + + if(itemConfig.action){ + item.attr('data-action', itemConfig.action); + if(itemConfig.target){ + item.attr('data-target', itemConfig.target); + } + if(itemConfig.data){ + item.data('payload', itemConfig.data); + } + } + + if(itemConfig.icon){ + item.prepend( + $('',{ + class: 'fas fa-fw ' + itemConfig.icon + }) + ); + } + break; + case 'heading': + item = $('
', { + class: 'panel-heading' + }).prepend( + $('

',{ + class: 'panel-title', + text: itemConfig.label + }) + ); + break; + } + + menu.append(item); + } + + return menu; + }; + + /** + * load left menu content options + * @param pageMenuLeftElement + * @returns {Promise} + */ + let loadLeftMenu = pageMenuLeftElement => { + + let executor = resolve => { + pageMenuLeftElement.append(getMenu([ + { + type: 'button', + label: 'Home', + icon: 'fa-home', + href: '/' + },{ + type: 'heading', + label: 'Information' + },{ + type: 'button', + label: 'Statistics', + icon: 'fa-chart-line', + btnType: 'info', + action: 'ShowStatsDialog' + },{ + type: 'button', + label: 'Wormhole data', + icon: 'fa-space-shuttle', + btnType: 'info', + action: 'ShowJumpInfo' + },{ + type: 'button', + label: 'Wormhole effects', + icon: 'fa-crosshairs', + btnType: 'info', + action: 'ShowSystemEffectInfo' + },{ + type: 'heading', + label: 'Settings' + },{ + type: 'button', + label: 'Account', + icon: 'fa-user', + action: 'ShowSettingsDialog' + }, document.fullscreenEnabled ? { + type: 'button', + id: Util.config.menuButtonFullScreenId, + label: 'Full screen', + icon: 'fa-expand-arrows-alt', + action: 'Fullscreen' + } : null,{ + type: 'button', + label: 'Notification test', + icon: 'fa-volume-up', + action: 'NotificationTest' + },{ + type: 'heading', + label: 'Danger zone' + },{ + type: 'button', + label: 'Delete account', + icon: 'fa-user-times', + btnType: 'danger', + action: 'DeleteAccount' + },{ + type: 'button', + label: 'Logout', + icon: 'fa-sign-in-alt', + btnType: 'warning', + action: 'Logout', + data: {clearCookies: 1} + } + ])); + + resolve({ + action: 'loadLeftMenu', + data: pageMenuLeftElement + }); + }; + + return new Promise(executor); + }; + + /** + * load right menu content options + * @param pageMenuRightElement + * @returns {Promise} + */ + let loadRightMenu = pageMenuRightElement => { + + let executor = resolve => { + pageMenuRightElement.append(getMenu([ + { + type: 'button', + label: 'Information', + icon: 'fa-street-view', + action: 'ShowMapInfo', + data: {tab: 'information'} + },{ + type: 'heading', + label: 'Configuration' + },{ + type: 'button', + class: 'loading', + label: 'Settings', + icon: 'fa-cogs', + action: 'ShowMapSettings', + data: {tab: 'settings'} + },{ + type: 'button', + id: Util.config.menuButtonGridId, + class: 'loading', + label: 'Grid snapping', + icon: 'fa-th', + action: 'MapOption', + target: 'map', + data: {option: 'mapSnapToGrid', toggle: true} + },{ + type: 'button', + id: Util.config.menuButtonMagnetizerId, + class: 'loading', + label: 'Magnetizing', + icon: 'fa-magnet', + action: 'MapOption', + target: 'map', + data: {option: 'mapMagnetizer', toggle: true} + },{ + type: 'button', + id: Util.config.menuButtonEndpointId, + class: 'loading', + label: 'Signatures', + icon: 'fa-link', + action: 'MapOption', + target: 'map', + data: {option: 'mapSignatureOverlays', toggle: true} + },{ + type: 'button', + id: Util.config.menuButtonCompactId, + class: 'loading', + label: 'Compact', + icon: 'fa-compress', + action: 'MapOption', + target: 'map', + data: {option: 'mapCompact', toggle: true} + },{ + type: 'heading', + label: 'Help' + },{ + type: 'button', + label: 'Manual', + icon: 'fa-book-reader', + btnType: 'info', + action: 'Manual' + },{ + type: 'button', + label: 'Shortcuts', + icon: 'fa-keyboard', + btnType: 'info', + action: 'Shortcuts' + },{ + type: 'button', + label: 'Task-Manager', + icon: 'fa-tasks', + btnType: 'info', + action: 'ShowTaskManager' + },{ + type: 'heading', + label: 'Danger zone' + },{ + type: 'button', + id: Util.config.menuButtonMapDeleteId, + label: 'Delete map', + icon: 'fa-trash', + btnType: 'danger', + action: 'DeleteMap' + } + ])); + + resolve({ + action: 'loadRightMenu', + data: pageMenuRightElement + }); + }; + + return new Promise(executor); + }; + + /** + * load page header + * @param pageElement + * @returns {Promise} + */ + let loadHeader = pageElement => { + + let executor = resolve => { + let moduleData = { + id: config.pageHeaderId, + logo: () => Mustache.render(TplLogo, {}), + brandLogo: config.menuHeadMenuLogoClass, + popoverTriggerClass: Util.config.popoverTriggerClass, + userCharacterClass: config.headUserCharacterClass, + userCharacterImageClass: config.userCharacterImageClass, + usersActiveClass: config.headActiveUsersClass, + userLocationId: Util.config.headUserLocationId, + mapTrackingId: Util.config.headMapTrackingId + }; + + pageElement.prepend(Mustache.render(TplHead, moduleData)); + + // init header -------------------------------------------------------------------------------------------- + + $('.' + config.headMenuClass).on('click.menuOpen', e => { + e.preventDefault(); + e.stopPropagation(); + Util.triggerMenuAction(document, 'Toggle', {menuClass: config.pageMenuLeftClass}); + }); + + $('.' + config.headMapClass).on('click.menuOpen', e => { + e.preventDefault(); + e.stopPropagation(); + Util.triggerMenuAction(document, 'Toggle', {menuClass: config.pageMenuRightClass}); + }); + + // active pilots + $('.' + config.headActiveUsersClass).on('click', () => { + Util.triggerMenuAction(document, 'ShowMapInfo', {tab: 'activity'}); + }); + + // current location + $('#' + Util.config.headUserLocationId).on('click', '>li', e => { + // this is CCPs systemId + let breadcrumbElement = $(e.currentTarget); + let systemId = parseInt(breadcrumbElement.attr('data-systemId')); + let systemName = breadcrumbElement.attr('data-systemName'); + + // ... we need to the PF systemId for 'SelectSystem' trigger + let activeMap = Util.getMapModule().getActiveMap(); + if(activeMap) { + let mapData = Util.getCurrentMapData(activeMap.data('id')); + let systemData = MapUtil.getSystemDataFromMapData(mapData, systemId, 'systemId'); + if(systemData){ + // system exists on map + Util.triggerMenuAction(activeMap, 'SelectSystem', { + systemId: systemData.id + }); + }else{ + // system not on map -> open 'add system' dialog + let options = { + systemData: { + id: systemId, + name: systemName + } + }; + + // -> check "previous" breadcrumb exists + // if exists, take it as "sourceSystem" + let breadcrumbElementPrev = breadcrumbElement.prev(); + if(breadcrumbElementPrev.length){ + let systemIdPrev = parseInt(breadcrumbElementPrev.attr('data-systemId')); + let systemDataPrev = MapUtil.getSystemDataFromMapData(mapData, systemIdPrev, 'systemId'); + if(systemDataPrev){ + let sourceSystem = $('#' + MapUtil.getSystemId(systemDataPrev.mapId, systemDataPrev.id)); + if(sourceSystem.length){ + options.sourceSystem = sourceSystem; + } + } + } + + Util.triggerMenuAction(activeMap, 'AddSystem', options); + } + }else{ + console.warn('No active map found!'); + } + }); + + // program status + $('.' + config.headProgramStatusClass).on('click', () => { + Util.triggerMenuAction(document, 'ShowTaskManager'); + }); + + // tracking toggle + let mapTrackingCheckbox = $('#' + Util.config.headMapTrackingId); + mapTrackingCheckbox.bootstrapToggle({ + size: 'mini', + on: 'on', + off: 'off', + onstyle: 'success', + offstyle: 'default', + width: 38, + height: 19 + }); + + // set default values for map tracking checkbox + // -> always "enable" + mapTrackingCheckbox.bootstrapToggle('on'); + + mapTrackingCheckbox.on('change', e => { + let value = $(e.target).is(':checked'); + let tracking = 'off'; + let trackingText = 'Your current location will not actually be added'; + let trackingType = 'info'; + if(value){ + tracking = 'on'; + trackingText = 'New connections will actually be added'; + trackingType = 'success'; + } + + Util.showNotify({title: 'Map tracking: ' + tracking, text: trackingText, type: trackingType}, false); + }); + + // init all tooltips + initHeaderTooltips($('#' + config.pageHeaderId)); + + resolve({ + action: 'loadHeader', + data: { + pageElement: pageElement + } + }); + }; + + return new Promise(executor); + }; + + /** + * load page footer + * @param pageElement + * @returns {Promise} + */ + let loadFooter = pageElement => { + + let executor = resolve => { + let moduleData = { + id: Util.config.footerId, + footerClockClass: config.footerClockClass, + footerLicenceLinkClass: config.footerLicenceLinkClass, + currentYear: new Date().getFullYear() + }; + + pageElement.prepend(Mustache.render(TplFooter, moduleData)); + + // init footer -------------------------------------------------------------------------------------------- + + pageElement.find('.' + config.footerLicenceLinkClass).on('click', e => { + e.stopPropagation(); + //show credits info dialog + $.fn.showCreditsDialog(); + }); + + initEveClock(); + + resolve({ + action: 'loadFooter', + data: { + pageElement: pageElement + } + }); + }; + + return new Promise(executor); + }; + + /** + * set page menu observer + * @param menuElement + * @returns {Promise} + */ + let setMenuObserver = menuElement => { + + let executor = resolve => { + + let getEventTarget = targetName => { + switch(targetName){ + case 'map': return Util.getMapModule().getActiveMap(); + case 'document': return document; + default: return document; + } + }; + + menuElement.on('click', '.list-group-item[data-action]', e => { + e.preventDefault(); + e.stopPropagation(); + + let button = e.currentTarget; + let action = button.getAttribute('data-action'); + let target = button.getAttribute('data-target'); + let data = $(button).data('payload'); + + Util.triggerMenuAction(getEventTarget(target), action, data); + }); + + resolve({ + action: 'setMenuObserver', + data: {} + }); + }; + + return new Promise(executor); + }; + + /** + * set global document observers + * @returns {Promise} + */ + let setDocumentObserver = () => { + + let executor = resolve => { + let documentElement = $(document); + + // init slide menus --------------------------------------------------------------------------------------- + let slideBarsController = new SlideBars(); + slideBarsController.init(); + + $('.' + config.pageClass).on('click.menuClose', () => { + slideBarsController.close(); + }); + + // menu event handling ------------------------------------------------------------------------------------ + documentElement.on('pf:menuAction', (e, action, data) => { + // menuAction events can also be triggered on child nodes (e.g. map) + // -> 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 'Close': + slideBarsController.close(); + break; + case 'Toggle': + slideBarsController.toggle(data.menuClass); + break; + case 'ShowStatsDialog': + $.fn.showStatsDialog(); + break; + case 'ShowJumpInfo': + $.fn.showJumpInfoDialog(); + break; + case 'ShowSystemEffectInfo': + $.fn.showSystemEffectInfoDialog(); + break; + case 'ShowSettingsDialog': + $.fn.showSettingsDialog(); + break; + case 'Fullscreen': + toggleFullScreen(document.body); + break; + case 'NotificationTest': + Util.showNotify({ + title: 'Test Notification', + text: 'Accept browser security question' + },{ + desktop: true, + stack: 'barBottom' + }); + break; + case 'DeleteAccount': + $.fn.showDeleteAccountDialog(); + break; + case 'Logout': + Util.logout({ + ajaxData: { + clearCookies: Util.getObjVal(data, 'clearCookies') || false + } + }); + break; + case'ShowMapInfo': + $.fn.showMapInfoDialog(data); + break; + case'ShowMapSettings': { + let mapData = false; + let activeMap = Util.getMapModule().getActiveMap(); + if(activeMap){ + mapData = Util.getCurrentMapData(activeMap.data('id')); + } + $.fn.showMapSettingsDialog(mapData, data); + break; + } + case'Manual': + $.fn.showMapManual(); + break; + case'Shortcuts': + $.fn.showShortcutsDialog(); + break; + case'ShowTaskManager': + Logging.showDialog(); + break; + case'DeleteMap': { + let mapData = false; + let activeMap = Util.getMapModule().getActiveMap(); + if(activeMap){ + mapData = activeMap.getMapDataFromClient(['hasId']); + } + $.fn.showDeleteMapDialog(mapData); + 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); + } + }); + + // disable menu links based on current map config --------------------------------------------------------- + documentElement.on('pf:updateMenuOptions', (e, data) => { + let hasRightMapDelete = MapUtil.checkRight('map_delete', data.mapConfig); + $('#' + Util.config.menuButtonMapDeleteId).toggleClass('disabled', !hasRightMapDelete); + + // "loading" menu options require an active map + // -> active map now available -> remove loading class + $('.' + config.pageMenuRightClass + ' .loading').removeClass('loading'); + }); + + // update header links with current map data -------------------------------------------------------------- + documentElement.on('pf:updateHeaderMapData', (e, data) => { + let activeMap = Util.getMapModule().getActiveMap(); + let userCountInside = 0; + let userCountOutside = 0; + let userCountInactive = 0; + + // show active user just for the current active map + if( + activeMap && + activeMap.data('id') === data.mapId + ){ + userCountInside = data.userCountInside; + userCountOutside = data.userCountOutside; + userCountInactive = data.userCountInactive; + } + updateHeaderActiveUserCount(userCountInside, userCountOutside, userCountInactive); + }); + + // changes in current userData ---------------------------------------------------------------------------- + documentElement.on('pf:changedUserData', (e, userData, changes) => { + updateHeaderUserData(userData, changes).then(); + }); + + // shutdown the program -> show dialog -------------------------------------------------------------------- + documentElement.on('pf:shutdown', (e, data) => { + // show shutdown dialog + let options = { + buttons: { + logout: { + label: ' restart', + className: ['btn-primary'].join(' '), + callback: function(){ + if(data.redirect) { + // ... redirect user to e.g. login form page ... + Util.redirect(data.redirect, ['logout']); + }else if(data.reload){ + // ... or reload current page ... + location.reload(); + }else{ + // ... fallback try to logout user + Util.triggerMenuAction(document, 'Logout'); + } + } + } + }, + content: { + icon: 'fa-bolt', + class: 'txt-color-danger', + title: 'Application error', + headline: 'Logged out', + text: [ + data.reason + ], + textSmaller: [] + } + }; + + // add error information (if available) + if(data.error && data.error.length){ + for(let error of data.error){ + options.content.textSmaller.push(error.message); + } + } + + $.fn.showNotificationDialog(options); + + documentElement.setProgramStatus('offline'); + + Util.showNotify({title: 'Logged out', text: data.reason, type: 'error'}, false); + + // remove map + Util.getMapModule().velocity('fadeOut', { + duration: 300, + complete: function(){ + $(this).remove(); + } + }); + + return false; + }); + + resolve({ + action: 'setDocumentObserver', + data: {} + }); + }; + + return new Promise(executor); + }; + + /** + * set global body observers + * @returns {Promise} + */ + let setBodyObserver = () => { + + let executor = resolve => { + let bodyElement = $(document.body); + + // global "popover" callback (for all popovers) + bodyElement.on('hide.bs.popover', '.' + Util.config.popoverTriggerClass, e => { + let popoverElement = $(e.target).data('bs.popover').tip(); + + // destroy all active tooltips inside this popover + popoverElement.destroyTooltip(true); + }); + + // global "modal" callback -------------------------------------------------------------------------------- + bodyElement.on('hide.bs.modal', '> .modal', e => { + let modalElement = $(e.target); + modalElement.destroyTimestampCounter(true); + + // destroy all popovers + modalElement.find('.' + Util.config.popoverTriggerClass).popover('destroy'); + + // destroy all Select2 + modalElement.find('.' + Util.config.select2Class) + .filter((i, element) => $(element).data('select2')) + .select2('destroy'); + }); + + // global "close" trigger for context menus --------------------------------------------------------------- + bodyElement.on('click.contextMenuClose', () => { + MapContextMenu.closeMenus(); + }); + + resolve({ + action: 'setBodyObserver', + data: {} + }); + }; + + return new Promise(executor); + }; + + /** + * set global shortcuts to element + * @returns {Promise|void|*} + */ + let setGlobalShortcuts = () => { + + let executor = resolve => { + let bodyElement = $(document.body); + + bodyElement.watchKey('tabReload', () => { location.reload(); }); - body.watchKey('newSignature', (body) => { + bodyElement.watchKey('renameSystem', () => { + let activeMap = Util.getMapModule().getActiveMap(); + if(activeMap){ + let activeSystem = activeMap.find('.' + MapUtil.config.systemActiveClass + ':first'); + if(activeSystem.length){ + MapUtil.toggleSystemAliasEditable(activeSystem); + } + } + }); + + bodyElement.watchKey('newSignature', () => { let activeMap = Util.getMapModule().getActiveMap(); if(activeMap){ let mapContentElement = MapUtil.getTabContentElementByMapElement(activeMap); @@ -145,7 +908,7 @@ define([ } }); - body.watchKey('clipboardPaste', (e) => { + bodyElement.watchKey('clipboardPaste', e => { // just send event to the current active map let activeMap = Util.getMapModule().getActiveMap(); if(activeMap){ @@ -173,685 +936,22 @@ define([ } } }); - }); - }; - /** - * get main menu title element - * @param title - * @returns {JQuery|*|jQuery} - */ - let getMenuHeadline = (title) => { - return $('
', { - class: 'panel-heading' - }).prepend( - $('

',{ - class: 'panel-title', - text: title - }) - ); - }; - - /** - * load left menu content options - */ - $.fn.loadLeftMenu = function(){ - - $(this).append( - $('
', { - class: 'list-group' - }).append( - $('', { - class: 'list-group-item', - href: '/' - }).html('  Home').prepend( - $('',{ - class: 'fas fa-home fa-fw' - }) - ) - ).append( - getMenuHeadline('Information') - ).append( - $('', { - class: 'list-group-item list-group-item-info' - }).html('  Statistics').prepend( - $('',{ - class: 'fas fa-chart-line fa-fw' - }) - ).on('click', function(){ - $(document).triggerMenuEvent('ShowStatsDialog'); - }) - ).append( - $('', { - class: 'list-group-item list-group-item-info' - }).html('  Effect info').prepend( - $('',{ - class: 'fas fa-crosshairs fa-fw' - }) - ).on('click', function(){ - $(document).triggerMenuEvent('ShowSystemEffectInfo'); - }) - ).append( - $('', { - class: 'list-group-item list-group-item-info' - }).html('  Jump info').prepend( - $('',{ - class: 'fas fa-space-shuttle fa-fw' - }) - ).on('click', function(){ - $(document).triggerMenuEvent('ShowJumpInfo'); - }) - ).append( - getMenuHeadline('Settings') - ).append( - $('', { - class: 'list-group-item' - }).html('  Account').prepend( - $('',{ - class: 'fas fa-user fa-fw' - }) - ).on('click', function(){ - $(document).triggerMenuEvent('ShowSettingsDialog'); - }) - ).append( - $('', { - class: 'list-group-item hide', // trigger by js - id: Util.config.menuButtonFullScreenId - }).html('  Full screen').prepend( - $('',{ - class: 'fas fa-expand-arrows-alt fa-fw' - }) - ).on('click', function(){ - let fullScreenElement = $('body'); - requirejs(['jquery', 'fullScreen'], function($){ - - if($.fullscreen.isFullScreen()){ - $.fullscreen.exit(); - }else{ - fullScreenElement.fullscreen({overflow: 'scroll', toggleClass: config.fullScreenClass}); - } - }); - }) - ).append( - $('', { - class: 'list-group-item' - }).html('  Notification test').prepend( - $('',{ - class: 'fas fa-volume-up fa-fw' - }) - ).on('click', function(){ - $(document).triggerMenuEvent('NotificationTest'); - }) - ).append( - getMenuHeadline('Danger zone') - ).append( - $('', { - class: 'list-group-item list-group-item-danger' - }).html('  Delete account').prepend( - $('',{ - class: 'fas fa-user-times fa-fw' - }) - ).on('click', function(){ - $(document).triggerMenuEvent('DeleteAccount'); - }) - ).append( - $('', { - class: 'list-group-item list-group-item-warning' - }).html('  Logout').prepend( - $('',{ - class: 'fas fa-sign-in-alt fa-fw' - }) - ).on('click', function(){ - $(document).triggerMenuEvent('Logout', {clearCookies: 1}); - }) - ).append( - $('
', { - class: config.menuClockClass - }) - ) - ); - - requirejs(['fullScreen'], function(){ - if($.fullscreen.isNativelySupported() === true){ - $('#' + Util.config.menuButtonFullScreenId).removeClass('hide'); - } - }); - }; - - /** - * load right content options - */ - $.fn.loadRightMenu = function(){ - $(this).append( - $('
', { - class: 'list-group' - }).append( - $('', { - class: 'list-group-item' - }).html('  Information').prepend( - $('',{ - class: 'fas fa-street-view fa-fw' - }) - ).on('click', function(){ - $(document).triggerMenuEvent('ShowMapInfo', {tab: 'information'}); - }) - ).append( - getMenuHeadline('Configuration') - ).append( - $('', { - class: 'list-group-item' - }).html('  Settings').prepend( - $('',{ - class: 'fas fa-cogs fa-fw' - }) - ).on('click', function(){ - $(document).triggerMenuEvent('ShowMapSettings', {tab: 'settings'}); - }) - ).append( - $('', { - class: 'list-group-item', - id: Util.config.menuButtonGridId - }).html('  Grid snapping').prepend( - $('',{ - class: 'fas fa-th fa-fw' - }) - ).on('click', function(){ - Util.getMapModule().getActiveMap().triggerMenuEvent('MapOption', { - option: 'mapSnapToGrid', - toggle: true - }); - }) - ).append( - $('', { - class: 'list-group-item', - id: Util.config.menuButtonMagnetizerId - }).html('  Magnetizing').prepend( - $('',{ - class: 'fas fa-magnet fa-fw' - }) - ).on('click', function(){ - Util.getMapModule().getActiveMap().triggerMenuEvent('MapOption', { - option: 'mapMagnetizer', - toggle: true - }); - }) - ).append( - $('', { - class: 'list-group-item', - id: Util.config.menuButtonEndpointId - }).html('  Signatures').prepend( - $('',{ - class: 'fas fa-link fa-fw' - }) - ).on('click', function(){ - Util.getMapModule().getActiveMap().triggerMenuEvent('MapOption', { - option: 'mapEndpoint', - toggle: true - }); - }) - ).append( - $('', { - class: 'list-group-item', - id: Util.config.menuButtonCompactId - }).html('  Compact').prepend( - $('',{ - class: 'fas fa-compress fa-fw' - }) - ).append( - $('',{ - class: 'badge bg-color bg-color-gray txt-color txt-color-warning', - text: 'beta' - }) - ).on('click', function(){ - Util.getMapModule().getActiveMap().triggerMenuEvent('MapOption', { - option: 'mapCompact', - toggle: true - }); - }) - ).append( - getMenuHeadline('Help') - ).append( - $('', { - class: 'list-group-item list-group-item-info' - }).html('  Manual').prepend( - $('',{ - class: 'fas fa-book-reader fa-fw' - }) - ).on('click', function(){ - $(document).triggerMenuEvent('Manual'); - }) - ).append( - $('', { - class: 'list-group-item list-group-item-info' - }).html('  Shortcuts').prepend( - $('',{ - class: 'fas fa-keyboard fa-fw' - }) - ).on('click', function(){ - $(document).triggerMenuEvent('Shortcuts'); - }) - ).append( - $('', { - class: 'list-group-item list-group-item-info' - }).html('  Task-Manager').prepend( - $('',{ - class: 'fas fa-tasks fa-fw' - }) - ).on('click', function(){ - $(document).triggerMenuEvent('ShowTaskManager'); - }) - ).append( - getMenuHeadline('Danger zone') - ).append( - $('', { - class: 'list-group-item list-group-item-danger', - id: Util.config.menuButtonMapDeleteId - }).html('  Delete map').prepend( - $('',{ - class: 'fas fa-trash fa-fw' - }) - ).on('click', function(){ - $(document).triggerMenuEvent('DeleteMap'); - }) - ) - ); - }; - - /** - * trigger menu event - * @param event - * @param data - */ - $.fn.triggerMenuEvent = function(event, data = {}){ - $(this).trigger('pf:menu' + event, [data]); - }; - - /** - * load page header - */ - $.fn.loadHeader = function(){ - let pageElement = $(this); - - let moduleData = { - id: config.pageHeaderId, - logo: function(){ - // render svg logo - return Mustache.render(TplLogo, {}); - }, - brandLogo: config.menuHeadMenuLogoClass, - popoverTriggerClass: Util.config.popoverTriggerClass, - userCharacterClass: config.headUserCharacterClass, - userCharacterImageClass: config.userCharacterImageClass, - userShipClass: config.headUserShipClass, - userShipImageClass: config.userShipImageClass, - mapTrackingId: Util.config.headMapTrackingId + resolve({ + action: 'setGlobalShortcuts', + data: {} + }); }; - let headRendered = Mustache.render(TplHead, moduleData); - - pageElement.prepend(headRendered); - - // init header ================================================================================================ - - // init slide menus - let slideMenu = new $.slidebars({ - scrollLock: false - }); - - // main menus - $('.' + config.headMenuClass).on('click', function(e){ - e.preventDefault(); - slideMenu.slidebars.toggle('left'); - }); - - $('.' + config.headMapClass).on('click', function(e){ - e.preventDefault(); - slideMenu.slidebars.toggle('right'); - }); - - // active pilots - $('.' + config.headActiveUserClass).on('click', function(){ - $(document).triggerMenuEvent('ShowMapInfo', {tab: 'activity'}); - }); - - // current location - $('#' + Util.config.headCurrentLocationId).find('a').on('click', function(){ - Util.getMapModule().getActiveMap().triggerMenuEvent('SelectSystem', {systemId: $(this).data('systemId')}); - }); - - // program status - $('.' + config.headProgramStatusClass).on('click', function(){ - $(document).triggerMenuEvent('ShowTaskManager'); - }); - - // close menu - $(document).on('pf:closeMenu', function(e){ - // close all menus - slideMenu.slidebars.close(); - }); - - // tracking toggle - let mapTrackingCheckbox = $('#' + Util.config.headMapTrackingId); - mapTrackingCheckbox.bootstrapToggle({ - size: 'mini', - on: 'on', - off: 'off', - onstyle: 'success', - offstyle: 'default', - width: 38, - height: 19 - }); - - // set default values for map tracking checkbox - // -> always "enable" - mapTrackingCheckbox.bootstrapToggle('on'); - - mapTrackingCheckbox.on('change', function(e){ - let value = $(this).is(':checked'); - let tracking = 'off'; - let trackingText = 'Your current location will not actually be added'; - let trackingType = 'info'; - if(value){ - tracking = 'on'; - trackingText = 'New connections will actually be added'; - trackingType = 'success'; - } - - Util.showNotify({title: 'Map tracking: ' + tracking, text: trackingText, type: trackingType}, false); - }); - - // init all tooltips - let tooltipElements = $('#' + config.pageHeaderId).find('[title]'); - tooltipElements.tooltip({ - placement: 'bottom', - delay: { - show: 500, - hide: 0 - } - }); - - return this; - }; - - /** - * load page footer - */ - $.fn.loadFooter = function(){ - let pageElement = $(this); - - let moduleData = { - id: Util.config.footerId, - footerLicenceLinkClass: config.footerLicenceLinkClass, - currentYear: new Date().getFullYear() - }; - - let footerElement = Mustache.render(TplFooter, moduleData); - - pageElement.prepend(footerElement); - - // init footer ================================================================================================ - pageElement.find('.' + config.footerLicenceLinkClass).on('click', function(){ - //show credits info dialog - $.fn.showCreditsDialog(); - }); - - return this; - }; - - /** - * catch all global document events - */ - let setPageObserver = function(){ - let documentElement = $(document); - let bodyElement = $(document.body); - - // on "full-screen" change event - documentElement.on('fscreenchange', function(e, state, elem){ - let menuButton = $('#' + Util.config.menuButtonFullScreenId); - if(state === true){ - // full screen active - - // close all menus - $(this).trigger('pf:closeMenu', [{}]); - - menuButton.addClass('active'); - }else{ - menuButton.removeClass('active'); - } - }); - - documentElement.on('pf:menuShowStatsDialog', function(e){ - // show user activity stats dialog - $.fn.showStatsDialog(); - return false; - }); - - documentElement.on('pf:menuShowSystemEffectInfo', function(e){ - // show system effects dialog - $.fn.showSystemEffectInfoDialog(); - return false; - }); - - documentElement.on('pf:menuShowJumpInfo', function(e){ - // show system effects info box - $.fn.showJumpInfoDialog(); - return false; - }); - - documentElement.on('pf:menuNotificationTest', function(e){ - // show system effects info box - notificationTest(); - return false; - }); - - documentElement.on('pf:menuDeleteAccount', function(e){ - // show "delete account" dialog - $.fn.showDeleteAccountDialog(); - return false; - }); - - documentElement.on('pf:menuManual', function(e){ - // show map manual - $.fn.showMapManual(); - return false; - }); - - documentElement.on('pf:menuShowTaskManager', function(e, data){ - // show log dialog - Logging.showDialog(); - return false; - }); - - documentElement.on('pf:menuShortcuts', function(e, data){ - // show shortcuts dialog - $.fn.showShortcutsDialog(); - return false; - }); - - documentElement.on('pf:menuShowSettingsDialog', function(e){ - // show character select dialog - $.fn.showSettingsDialog(); - return false; - }); - - documentElement.on('pf:menuShowMapInfo', function(e, data){ - // show map information dialog - $.fn.showMapInfoDialog(data); - return false; - }); - - documentElement.on('pf:menuShowMapSettings', function(e, data){ - // show map edit dialog or edit map - let mapData = false; - - let activeMap = Util.getMapModule().getActiveMap(); - - if(activeMap){ - mapData = Util.getCurrentMapData( activeMap.data('id') ); - } - - $.fn.showMapSettingsDialog(mapData, data); - return false; - }); - - documentElement.on('pf:menuDeleteMap', function(e){ - // delete current active map - let mapData = false; - - let activeMap = Util.getMapModule().getActiveMap(); - - if(activeMap){ - mapData = activeMap.getMapDataFromClient(['hasId']); - } - - $.fn.showDeleteMapDialog(mapData); - return false; - }); - - documentElement.on('pf:menuLogout', function(e, data){ - - let clearCookies = false; - if( - typeof data === 'object' && - data.hasOwnProperty('clearCookies') - ){ - clearCookies = data.clearCookies; - } - - // logout - Util.logout({ - ajaxData: { - clearCookies: clearCookies - } - }); - return false; - }); - - // END menu events ============================================================================================ - - // global "popover" callback (for all popovers) - $('.' + Util.config.popoverTriggerClass).on('hide.bs.popover', function(e){ - let popoverElement = $(this).data('bs.popover').tip(); - - // destroy all active tooltips inside this popover - popoverElement.destroyTooltip(true); - }); - - // global "modal" callback (for all modals) - bodyElement.on('hide.bs.modal', '> .modal', function(e){ - let modalElement = $(this); - modalElement.destroyTimestampCounter(true); - - // destroy all Select2 - modalElement.find('.' + Util.config.select2Class) - .filter((i, element) => $(element).data('select2')) - .select2('destroy'); - }); - - // global "close" trigger for context menus - bodyElement.on('click.contextMenuClose', function(e){ - MapContextMenu.closeMenus(); - }); - - // disable menu links based on current map config - documentElement.on('pf:updateMenuOptions', function(e, data){ - let hasRightMapDelete = MapUtil.checkRight('map_delete', data.mapConfig); - $('#' + Util.config.menuButtonMapDeleteId).toggleClass('disabled', !hasRightMapDelete); - }); - - // update header links with current map data - documentElement.on('pf:updateHeaderMapData', function(e, data){ - let activeMap = Util.getMapModule().getActiveMap(); - - let userCountInside = 0; - let userCountOutside = 0; - let userCountInactive = 0; - let currentLocationData = {}; - - // show active user just for the current active map - if( - activeMap && - activeMap.data('id') === data.mapId - ){ - userCountInside = data.userCountInside; - userCountOutside = data.userCountOutside; - userCountInactive = data.userCountInactive; - currentLocationData = data.currentLocation; - } - updateHeaderActiveUserCount(userCountInside, userCountOutside, userCountInactive); - updateHeaderCurrentLocation(currentLocationData); - }); - - // shutdown the program -> show dialog - documentElement.on('pf:shutdown', function(e, data){ - // show shutdown dialog - let options = { - buttons: { - logout: { - label: ' restart', - className: ['btn-primary'].join(' '), - callback: function(){ - if(data.redirect) { - // ... redirect user to e.g. login form page ... - Util.redirect(data.redirect, ['logout']); - }else if(data.reload){ - // ... or reload current page ... - location.reload(); - }else{ - // ... fallback try to logout user - documentElement.trigger('pf:menuLogout'); - } - } - } - }, - content: { - icon: 'fa-bolt', - class: 'txt-color-danger', - title: 'Application error', - headline: 'Logged out', - text: [ - data.reason - ], - textSmaller: [] - } - }; - - // add error information (if available) - if(data.error && data.error.length){ - for(let error of data.error){ - options.content.textSmaller.push(error.message); - } - } - - $.fn.showNotificationDialog(options); - - documentElement.setProgramStatus('offline'); - - Util.showNotify({title: 'Logged out', text: data.reason, type: 'error'}, false); - - // remove map --------------------------------------------------------------------------------------------- - Util.getMapModule().velocity('fadeOut', { - duration: 300, - complete: function(){ - $(this).remove(); - } - }); - - return false; - }); - - initEveClock(); + return new Promise(executor); }; /** * init clock element with current EVE time */ let initEveClock = () => { - let clockElement = $('.' + config.menuClockClass); - - let checkTime = (i) => { - return (i < 10) ? '0' + i : i; - }; + let clockElement = $('.' + config.footerClockClass); + let checkTime = i => (i < 10) ? '0' + i : i; let startTime = () => { let date = Util.getServerTime(); @@ -859,172 +959,164 @@ define([ let m = checkTime(date.getMinutes()); clockElement.text(h + ':' + m); - let t = setTimeout(startTime, 500); + setTimeout(startTime, 500); }; startTime(); }; /** - * updates the header with current user data + * update all header elements with current userData + * @param userData + * @param changes + * @returns {Promise<[any, any, any, any, any, any, any, any, any, any]>} */ - $.fn.updateHeaderUserData = function(){ - let userData = Util.getCurrentUserData(); + let updateHeaderUserData = (userData, changes) => { + let updateTasks = []; - let userInfoElement = $('.' + config.headUserCharacterClass); - let currentCharacterId = userInfoElement.data('characterId'); - let currentCharactersOptionIds = userInfoElement.data('characterOptionIds') ? userInfoElement.data('characterOptionIds') : []; - let newCharacterId = 0; - let newCharacterName = ''; - - let userShipElement = $('.' + config.headUserShipClass); - let currentShipData = userShipElement.data('shipData'); - let currentShipId = Util.getObjVal(currentShipData, 'typeId') || 0; - let newShipData = { - typeId: 0, - typeName: '' - }; - - // function for header element toggle animation - let animateHeaderElement = (element, callback, triggerShow) => { - let currentOpacity = parseInt(element.css('opacity')); - - let showHeaderElement = (element) => { - element.show().velocity({ - opacity: [ 1, 0 ] - },{ - // display: 'block', - visibility : 'visible', - duration: 1000 - }); - }; - - let hideHeaderElement = (element, callback) => { - element.velocity('stop').velocity({ - opacity: [ 0, 1 ] - },{ - // display: 'none', - visibility : 'hidden', - duration: 1000, - complete: function(){ - element.hide(); - // callback - callback($(this)); - } - }); - }; - - // run show/hide toggle in the correct order - if(currentOpacity > 0 && triggerShow){ - // hide then show animation - hideHeaderElement(element, (element) => { - callback(element); - showHeaderElement(element); - }); - }else if(currentOpacity > 0 && !triggerShow){ - // hide animation - hideHeaderElement(element, (element) => { - element.hide(); - callback(element); - }); - }else if(currentOpacity === 0 && triggerShow){ - // show animation - callback(element); - showHeaderElement(element); - }else{ - // no animation - callback(element); - } - }; - - // check for character/ship changes --------------------------------------------------------------------------- - if( - userData && - userData.character - ){ - newCharacterId = userData.character.id; - newCharacterName = userData.character.name; - - if(userData.character.log){ - newShipData = userData.character.log.ship; - } - - // en/disable "map tracking" toggle - updateMapTrackingToggle(userData.character.logLocation); + if(changes.characterLogLocation){ + updateTasks.push(updateMapTrackingToggle(Boolean(Util.getObjVal(userData, 'character.logLocation')))); + } + if(changes.charactersIds){ + updateTasks.push(updateHeaderCharacterSwitch(userData, changes.characterId)); + } + if(changes.characterSystemId || changes.characterShipType){ + updateTasks.push(updateHeaderCharacterLocation(userData, changes.characterShipType)); } - let newCharactersOptionIds = userData.characters.map(function(data){ - return data.id; - }); - - // update user character data --------------------------------------------------------------------------------- - if(currentCharactersOptionIds.toString() !== newCharactersOptionIds.toString()){ - - let currentCharacterChanged = false; - if(currentCharacterId !== newCharacterId){ - currentCharacterChanged = true; - } - - // toggle element - animateHeaderElement(userInfoElement, (userInfoElement) => { - if(currentCharacterChanged){ - userInfoElement.find('span').text( newCharacterName ); - userInfoElement.find('img').attr('src', Init.url.ccpImageServer + '/Character/' + newCharacterId + '_32.jpg'); - } - // init "character switch" popover - userInfoElement.initCharacterSwitchPopover(userData); - }, true); - - // store new id(s) for next check - userInfoElement.data('characterId', newCharacterId); - userInfoElement.data('characterOptionIds', newCharactersOptionIds); - } - - // update user ship data -------------------------------------------------------------------------------------- - if(currentShipId !== newShipData.typeId){ - // set new data for next check - userShipElement.data('shipData', newShipData); - - let showShipElement = newShipData.typeId > 0; - - // toggle element - animateHeaderElement(userShipElement, (userShipElement) => { - userShipElement.find('span').text( newShipData.typeName ); - userShipElement.find('img').attr('src', Init.url.ccpImageServer + '/Render/' + newShipData.typeId + '_32.png'); - // trigger ship change event - $(document).trigger('pf:activeShip', { - shipData: newShipData - }); - }, showShipElement); - } + return Promise.all(updateTasks); }; /** * update "map tracking" toggle in header * @param status */ - let updateMapTrackingToggle = function(status){ - let mapTrackingCheckbox = $('#' + Util.config.headMapTrackingId); - if(status === true){ - mapTrackingCheckbox.bootstrapToggle('enable'); - }else{ - mapTrackingCheckbox.bootstrapToggle('off').bootstrapToggle('disable'); - } + let updateMapTrackingToggle = status => { + let executor = resolve => { + let mapTrackingCheckbox = $('#' + Util.config.headMapTrackingId); + if(status === true){ + mapTrackingCheckbox.bootstrapToggle('enable'); + }else{ + mapTrackingCheckbox.bootstrapToggle('off').bootstrapToggle('disable'); + } + + resolve({ + action: 'updateMapTrackingToggle', + data: {} + }); + }; + + return new Promise(executor); }; /** - * delete active character log for the current user + * @param userData + * @param changedCharacter + * @returns {Promise} */ - let deleteLog = function(){ + let updateHeaderCharacterSwitch = (userData, changedCharacter) => { + let executor = resolve => { + let userInfoElement = $('.' + config.headUserCharacterClass); + // toggle element + animateHeaderElement(userInfoElement, userInfoElement => { + if(changedCharacter){ + // current character changed + userInfoElement.find('span').text(Util.getObjVal(userData, 'character.name')); + userInfoElement.find('img').attr('src', Init.url.ccpImageServer + '/Character/' + Util.getObjVal(userData, 'character.id') + '_32.jpg'); + } + // init "character switch" popover + userInfoElement.initCharacterSwitchPopover(userData); - $.ajax({ - type: 'POST', - url: Init.path.deleteLog, - data: {}, - dataType: 'json' - }).done(function(data){ + resolve({ + action: 'updateHeaderCharacterSwitch', + data: {} + }); + }, true); + }; - }); + return new Promise(executor); + }; + + /** + * + * @param userData + * @param changedShip + * @returns {Promise} + */ + let updateHeaderCharacterLocation = (userData, changedShip) => { + let executor = resolve => { + let userLocationElement = $('#' + Util.config.headUserLocationId); + let breadcrumbHtml = ''; + let logData = Util.getObjVal(userData, 'character.log'); + let logDataAll = []; + + if(logData){ + let shipData = Util.getObjVal(userData, 'character.log.ship'); + let shipTypeId = Util.getObjVal(shipData, 'typeId') || 0; + let shipTypeName = Util.getObjVal(shipData, 'typeName') || ''; + + logDataAll.push(logData); + + // check for log history data as well + let logHistoryData = Util.getObjVal(userData, 'character.logHistory'); + if(logHistoryData){ + // check if there are more history log entries than max visual limit + if(logHistoryData.length > config.headMaxLocationHistoryBreadcrumbs){ + breadcrumbHtml += '
  • '; + breadcrumbHtml += ''; + breadcrumbHtml += '…'; + breadcrumbHtml += '
  • '; + } + logDataAll = logDataAll.concat(logHistoryData.map(data => data.log).slice(0, config.headMaxLocationHistoryBreadcrumbs)); + } + + logDataAll = logDataAll.reverse(); + + // build breadcrumb + for(let [key, logDataTmp] of logDataAll.entries()){ + let systemId = Util.getObjVal(logDataTmp, 'system.id'); + let systemName = Util.getObjVal(logDataTmp, 'system.name'); + let isCurrentLocation = key === logDataAll.length -1; + let visibleClass = !isCurrentLocation ? 'hidden-xs' : ''; + + breadcrumbHtml += '
  • '; + breadcrumbHtml += ''; + + if(isCurrentLocation){ + breadcrumbHtml += ''; + } + + breadcrumbHtml += systemName; + + if(isCurrentLocation && shipTypeId){ + // show ship image + let shipSrc = Init.url.ccpImageServer + '/Render/' + shipTypeId + '_32.png'; + breadcrumbHtml += ' { + userLocationElement.html(breadcrumbHtml); + initHeaderTooltips(userLocationElement); + + if(changedShip){ + $(document).trigger('pf:activeShip'); + } + }, Boolean(logData)); + + resolve({ + action: 'updateHeaderCharacterLocation', + data: {} + }); + }; + + return new Promise(executor); }; /** @@ -1034,7 +1126,7 @@ define([ * @param userCountInactive */ let updateHeaderActiveUserCount = (userCountInside, userCountOutside, userCountInactive) => { - let activeUserElement = $('.' + config.headActiveUserClass); + let activeUserElement = $('.' + config.headActiveUsersClass); let updateCount = (badge, count) => { let changed = false; @@ -1054,7 +1146,7 @@ define([ let changedInactive = updateCount(activeUserElement.find('.badge[data-type="inactive"]'), userCountInactive); if( - (changedInactive || changedOutside || changedInactive) && + (changedInside || changedOutside || changedInactive) && !activeUserElement.is(':visible') ){ activeUserElement.velocity('fadeIn', {duration: Init.animationSpeed.headerLink}); @@ -1062,60 +1154,62 @@ define([ }; /** - * update the "current location" link element in head - * @param locationData + * function for header element toggle animation + * @param element + * @param callback + * @param triggerShow */ - let updateHeaderCurrentLocation = locationData => { - let systemId = locationData.id || 0; - let systemName = locationData.name || false; + let animateHeaderElement = (element, callback, triggerShow) => { + let currentOpacity = parseInt(element.css('opacity')); - let currentLocationData = Util.getCurrentLocationData(); + let showHeaderElement = (element) => { + element.show().velocity({ + opacity: [ 1, 0 ] + },{ + // display: 'block', + visibility : 'visible', + duration: Init.animationSpeed.headerLink + }); + }; - if( - currentLocationData.name !== systemName || - currentLocationData.id !== systemId - ){ - Util.setCurrentLocationData(systemId, systemName); - - let currentLocationElement = $('#' + Util.config.headCurrentLocationId); - let linkElement = currentLocationElement.find('a'); - linkElement.toggleClass('disabled', !systemId); - - if(systemName !== false){ - linkElement.find('span').text(locationData.name); - currentLocationElement.velocity('fadeIn', {duration: Init.animationSpeed.headerLink}); - }else{ - if(currentLocationElement.is(':visible')){ - currentLocationElement.velocity('fadeOut', {duration: Init.animationSpeed.headerLink}); + let hideHeaderElement = (element, callback) => { + element.velocity('stop').velocity({ + opacity: [ 0, 1 ] + },{ + // display: 'none', + visibility : 'hidden', + duration: Init.animationSpeed.headerLink, + complete: function(){ + element.hide(); + // callback + callback($(this)); } - } + }); + }; - // auto select current system ----------------------------------------------------------------------------- - let userData = Util.getCurrentUserData(); - - if( - Boolean(Util.getObjVal(Init, 'character.autoLocationSelect')) && - Util.getObjVal(userData, 'character.selectLocation') - ){ - Util.getMapModule().getActiveMap().triggerMenuEvent('SelectSystem', {systemId: systemId, forceSelect: false}); - } + // run show/hide toggle in the correct order + if(currentOpacity > 0 && triggerShow){ + // hide then show animation + hideHeaderElement(element, (element) => { + callback(element); + showHeaderElement(element); + }); + }else if(currentOpacity > 0 && !triggerShow){ + // hide animation + hideHeaderElement(element, (element) => { + element.hide(); + callback(element); + }); + }else if(currentOpacity === 0 && triggerShow){ + // show animation + callback(element); + showHeaderElement(element); + }else{ + // no animation + callback(element); } }; - /** - * shows a test notification for desktop messages - */ - let notificationTest = () => { - Util.showNotify({ - title: 'Test Notification', - text: 'Accept browser security question'}, - { - desktop: true, - stack: 'barBottom' - } - ); - }; - /** * set event listener if the program tab is active or not * this is used to lower the update ping cycle to reduce server load @@ -1205,8 +1299,8 @@ define([ let icon = statusElement.find('i'); let textElement = statusElement.find('span'); - let iconClass = false; - let textClass = false; + let iconClass = ''; + let textClass = ''; switch(status){ case 'online': @@ -1233,11 +1327,11 @@ define([ programStatusInterval = false; } - if( statusElement.data('status') !== status ){ + if(statusElement.data('status') !== status){ // status has changed - if(! programStatusInterval){ + if(!programStatusInterval){ // check if timer exists if not -> set default (in case of the "init" ajax call failed - let programStatusVisible = Init.timer ? Init.timer.PROGRAM_STATUS_VISIBLE : 5000; + let programStatusVisible = Util.getObjVal(Init, 'timer.PROGRAM_STATUS_VISIBLE') || 5000; let timer = function(){ // change status on first timer iteration @@ -1321,8 +1415,8 @@ define([ }; return { + loadPageStructure: loadPageStructure, initTabChangeObserver: initTabChangeObserver, renderMapContextMenus: renderMapContextMenus }; - }); \ No newline at end of file diff --git a/js/app/setup.js b/js/app/setup.js index faf5f650..c3c1c57a 100644 --- a/js/app/setup.js +++ b/js/app/setup.js @@ -6,12 +6,15 @@ define([ 'jquery', 'app/init', 'app/util', - 'app/map/worker' -], function($, Init, Util, MapWorker){ + 'app/map/worker', + 'mustache', +], function($, Init, Util, MapWorker, Mustache){ 'use strict'; let config = { - splashOverlayClass: 'pf-splash' // class for "splash" overlay + splashOverlayClass: 'pf-splash', // class for "splash" overlay + webSocketStatsId: 'pf-setup-webSocket-stats', // id for webSocket "stats" panel + webSocketRefreshStatsId: 'pf-setup-webSocket-stats-refresh' // class for "reload stats" button }; /** @@ -42,6 +45,18 @@ define([ }); }; + /** + * + * @param container + * @param selector + */ + let setCollapseObserver = (container, selector) => { + container.find(selector).css({cursor: 'pointer'}); + container.on('click', selector, function(){ + $(this).find('.pf-animate-rotate').toggleClass('right'); + }); + }; + /** * set page observer */ @@ -52,9 +67,7 @@ define([ Util.initPageScroll(body); // collapse --------------------------------------------------------------------------------------------------- - body.find('[data-toggle="collapse"]').css({cursor: 'pointer'}).on('click', function(){ - $(this).find('.pf-animate-rotate').toggleClass('right'); - }); + setCollapseObserver(body, '[data-toggle="collapse"]'); // buttons ---------------------------------------------------------------------------------------------------- // exclude "download" && "navigation" buttons @@ -128,6 +141,29 @@ define([ } }; + /** + * get WebSockets "subscriptions" HTML + * @param subscriptionStats + * @returns {Promise} + */ + let getWebSocketSubscriptionTable = subscriptionStats => { + + let executor = resolve => { + requirejs(['text!templates/modules/subscriptions_table.html', 'mustache'], (template, Mustache) => { + let data = { + panelId: config.webSocketStatsId, + refreshButtonId: config.webSocketRefreshStatsId, + subStats: subscriptionStats, + channelCount: (Util.getObjVal(subscriptionStats, 'channels') || []).length + }; + + resolve(Mustache.render(template, data)); + }); + }; + + return new Promise(executor); + }; + /** * perform a basic check if Clients (browser) can connect to the webSocket server */ @@ -158,7 +194,7 @@ define([ let socketDangerCount = parseInt(badgeSocketDanger.text()) || 0; if(data.uri){ - let uriRow = webSocketPanel.find('.panel-body table tr'); + let uriRow = webSocketPanel.find('.panel-body').filter(':first').find('table tr'); uriRow.find('td:nth-child(2) kbd').html(data.uri.value); if(data.uri.status){ let statusIcon = uriRow.find('td:nth-child(3) i'); @@ -206,6 +242,18 @@ define([ } }); + /** + * @param socket + * @param task + * @param load + */ + let sendMessage = (socket, task, load) => { + socket.send(JSON.stringify({ + task: task, + load: load + })); + }; + // try to connect to WebSocket server let socket = new WebSocket(webSocketURI); @@ -219,10 +267,7 @@ define([ }); // sent token and check response - socket.send(JSON.stringify({ - task: 'healthCheck', - load: tcpSocketPanel.data('token') - })); + sendMessage(socket, 'healthCheck', tcpSocketPanel.attr('data-token')); webSocketPanel.hideLoadingAnimation(); }; @@ -230,7 +275,7 @@ define([ socket.onmessage = (e) => { let response = JSON.parse(e.data); - if(response === 1){ + if(Util.getObjVal(response, 'load.isValid') === true){ // SUCCESS updateWebSocketPanel({ status: { @@ -239,6 +284,23 @@ define([ class: 'txt-color-success' } }); + + // show subscription stats table + getWebSocketSubscriptionTable(Util.getObjVal(response, 'load.subStats')).then(payload => { + // remove existing table -> then insert new + $('#' + config.webSocketStatsId).remove(); + $(payload).insertAfter(webSocketPanel).initTooltips(); + + let token = Util.getObjVal(response, 'load.token'); + tcpSocketPanel.attr('data-token', token); + + // set "reload stats" observer + $('#' + config.webSocketRefreshStatsId).on('click', function(){ + $('#' + config.webSocketStatsId).showLoadingAnimation(); + + sendMessage(socket, 'healthCheck', token); + }); + }); }else{ // Got response but INVALID updateWebSocketPanel({ @@ -274,6 +336,8 @@ define([ }); webSocketPanel.hideLoadingAnimation(); + + $('#' + config.webSocketStatsId).remove(); }; }; @@ -286,7 +350,7 @@ define([ Util.showVersionInfo(); // hide splash loading animation ------------------------------------------------------------------------------ - $('.' + config.splashOverlayClass).hideSplashOverlay(); + $('.' + config.splashOverlayClass + '[data-status="ok"]').hideSplashOverlay(); setPageObserver(); diff --git a/js/app/ui/dialog/account_settings.js b/js/app/ui/dialog/account_settings.js index d8f94592..2a4a1de7 100644 --- a/js/app/ui/dialog/account_settings.js +++ b/js/app/ui/dialog/account_settings.js @@ -6,9 +6,8 @@ define([ 'jquery', 'app/init', 'app/util', - 'app/render', 'bootbox' -], function($, Init, Util, Render, bootbox){ +], ($, Init, Util, bootbox) => { 'use strict'; let config = { @@ -130,10 +129,9 @@ define([ Util.showNotify({title: 'Account saved', type: 'success'}); // close dialog/menu - $(document).trigger('pf:closeMenu', [{}]); + Util.triggerMenuAction(document, 'Close'); accountSettingsDialog.modal('hide'); } - }).fail(function(jqXHR, status, error){ accountSettingsDialog.find('.modal-content').hideLoadingAnimation(); diff --git a/js/app/ui/dialog/api_status.js b/js/app/ui/dialog/api_status.js index 492bf8dc..97ab944c 100644 --- a/js/app/ui/dialog/api_status.js +++ b/js/app/ui/dialog/api_status.js @@ -6,9 +6,8 @@ define([ 'jquery', 'app/init', 'app/util', - 'app/render', 'bootbox' -], ($, Init, Util, Render, bootbox) => { +], ($, Init, Util, bootbox) => { 'use strict'; let config = { diff --git a/js/app/ui/dialog/changelog.js b/js/app/ui/dialog/changelog.js index 75b393a0..144c0447 100644 --- a/js/app/ui/dialog/changelog.js +++ b/js/app/ui/dialog/changelog.js @@ -6,9 +6,8 @@ define([ 'jquery', 'app/init', 'app/util', - 'app/render', 'bootbox' -], ($, Init, Util, Render, bootbox) => { +], ($, Init, Util, bootbox) => { 'use strict'; let config = { diff --git a/js/app/ui/dialog/credit.js b/js/app/ui/dialog/credit.js index b3e75b24..7728f056 100644 --- a/js/app/ui/dialog/credit.js +++ b/js/app/ui/dialog/credit.js @@ -6,10 +6,9 @@ define([ 'jquery', 'app/init', 'app/util', - 'app/render', 'bootbox', - 'app/ui/logo' -], function($, Init, Util, Render, bootbox){ + 'layout/logo' +], ($, Init, Util, bootbox) => { 'use strict'; let config = { @@ -23,7 +22,7 @@ define([ */ $.fn.showCreditsDialog = function(callback, enableHover){ - requirejs(['text!templates/dialog/credit.html', 'mustache'], function(template, Mustache){ + requirejs(['text!templates/dialog/credit.html', 'mustache'], (template, Mustache) => { let data = { logoContainerId: config.creditsDialogLogoContainerId, diff --git a/js/app/ui/dialog/jump_info.js b/js/app/ui/dialog/jump_info.js index 6f7f76cd..39cc30e8 100644 --- a/js/app/ui/dialog/jump_info.js +++ b/js/app/ui/dialog/jump_info.js @@ -6,15 +6,21 @@ define([ 'jquery', 'app/init', 'app/util', - 'app/render', 'bootbox', -], ($, Init, Util, Render, bootbox) => { + 'app/map/util' +], ($, Init, Util, bootbox, MapUtil) => { 'use strict'; let config = { // jump info dialog jumpInfoDialogClass: 'pf-jump-info-dialog', // class for jump info dialog + + wormholeInfoDialogListId: 'pf-wormhole-info-dialog-list', // id for map "list" container + wormholeInfoDialogStaticId: 'pf-wormhole-info-dialog-static', // id for map "static" container + wormholeInfoDialogJumpId: 'pf-wormhole-info-dialog-jump', // id for map "jump" container + wormholeInfoMassTableClass: 'pf-wormhole-info-mass-table', // class for "wormhole mass" table + wormholeInfoStaticTableClass: 'pf-wormhole-info-static-table', // class for "static" table wormholeInfoJumpTableClass: 'pf-wormhole-info-jump-table' // class for "wormhole jump" table }; @@ -23,15 +29,40 @@ define([ */ $.fn.showJumpInfoDialog = function(){ requirejs(['text!templates/dialog/jump_info.html', 'mustache', 'datatables.loader'], (template, Mustache) => { + + let staticsMatrixHead = [ + ['From╲To', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'H', 'L', '0.0', 'C12', 'C13'] + ]; + + let staticsMatrixBody = [ + ['C1', 'H121', 'C125', 'O883', 'M609', 'L614', 'S804', 'N110', 'J244', 'Z060', 'F353', ''], + ['C2', 'Z647', 'D382', 'O477', 'Y683', 'N062', 'R474', 'B274', 'A239', 'E545', 'F135', ''], + ['C3', 'V301', 'I182', 'N968', 'T405', 'N770', 'A982', 'D845', 'U210', 'K346', 'F135', ''], + ['C4', 'P060', 'N766', 'C247', 'X877', 'H900', 'U574', 'S047', 'N290', 'K329', '' , ''], + ['C5', 'Y790', 'D364', 'M267', 'E175', 'H296', 'V753', 'D792', 'C140', 'Z142', '' , ''], + ['C6', 'Q317', 'G024', 'L477', 'Z457', 'V911', 'W237', ['B520', 'D792'], ['C140', 'C391'], ['C248', 'Z142'], '', ''], + ['H', 'Z971', 'R943', 'X702', 'O128', 'M555', 'B041', 'A641', 'R051', 'V283', 'T458', ''], + ['L', 'Z971', 'R943', 'X702', 'O128', 'N432', 'U319', 'B449', 'N944', 'S199', 'M164', ''], + ['0.0', 'Z971', 'R943', 'X702', 'O128', 'N432', 'U319', 'B449', 'N944', 'S199', 'L031', ''], + ['C12', '' , '' , '' , '' , '' , '' , 'Q063', 'V898', 'E587', '' , ''], + ['?', 'E004', 'L005', 'Z006', 'M001', 'C008', 'G008', '' , '' , 'Q003', '' , 'A009'] + ]; + let data = { config: config, + popoverTriggerClass: Util.config.popoverTriggerClass, wormholes: Object.keys(Init.wormholes).map(function(k){ return Init.wormholes[k]; }), // convert Json to array - securityClass: function(){ - return function(value, render){ - return this.Util.getSecurityClassForSystem( render(value) ); - }.bind(this); - }.bind({ - Util: Util + staticsMatrixHead: staticsMatrixHead, + staticsMatrixBody: staticsMatrixBody.map((row, rowIndex) => { + return row.map((label, colIndex) => { + // get security name from "matrix Head" data if NOT first column + let secName = colIndex ? staticsMatrixHead[0][colIndex] : label; + return { + label: label, + class: Util.getSecurityClassForSystem(secName), + hasPopover: colIndex && label.length + }; + }); }), massValue: function(){ return function(value, render){ @@ -66,22 +97,26 @@ define([ let float = render(value); return float.length ? parseFloat(float).toLocaleString() + ' %' : 'unknown'; }; + }, + securityClass: function(){ + return function(value, render){ + return Util.getSecurityClassForSystem(this); + }; } }; let content = Mustache.render(template, data); let jumpDialog = bootbox.dialog({ className: config.jumpInfoDialogClass, - title: 'Wormhole jump information', + title: 'Wormhole data', message: content, show: false }); jumpDialog.on('show.bs.modal', function(e){ - // init dataTable $(this).find('.' + config.wormholeInfoMassTableClass).DataTable({ - pageLength: 25, - lengthMenu: [[10, 20, 25, 30, 40, -1], [10, 20, 25, 30, 40, 'All']], + pageLength: 35, + lengthMenu: [[15, 25, 35, 50, -1], [15, 25, 35, 50, 'All']], autoWidth: false, language: { emptyTable: 'No wormholes', @@ -93,6 +128,18 @@ define([ data: null // use DOM data overwrites [] default -> data.loader.js }); + $(this).find('.' + config.wormholeInfoStaticTableClass).DataTable({ + pageLength: -1, + paging: false, + lengthChange: false, + ordering: false, + searching: false, + info: false, + autoWidth: false, + columnDefs: [], + data: null // use DOM data overwrites [] default -> data.loader.js + }); + $(this).find('.' + config.wormholeInfoJumpTableClass).DataTable({ pageLength: -1, paging: false, @@ -110,6 +157,11 @@ define([ columnDefs: [], data: null // use DOM data overwrites [] default -> data.loader.js }); + + MapUtil.initWormholeInfoTooltip( + $(this).find('.' + config.wormholeInfoStaticTableClass), + '.' + Util.config.popoverTriggerClass + ); }); jumpDialog.initTooltips(); diff --git a/js/app/ui/dialog/manual.js b/js/app/ui/dialog/manual.js index 8c1c141d..62285ae7 100644 --- a/js/app/ui/dialog/manual.js +++ b/js/app/ui/dialog/manual.js @@ -6,9 +6,8 @@ define([ 'jquery', 'app/init', 'app/util', - 'app/render', 'bootbox', -], ($, Init, Util, Render, bootbox) => { +], ($, Init, Util, bootbox) => { 'use strict'; @@ -62,23 +61,17 @@ define([ let disableOnScrollEvent = false; // scroll breakpoints - let scrolLBreakpointElements = null; + let scrollBreakpointElements = $('.pf-manual-scroll-break'); // scroll navigation links - let scrollNavLiElements = null; - - mapManualDialog.on('shown.bs.modal', function(e){ - // modal on open - scrolLBreakpointElements = $('.pf-manual-scroll-break'); - scrollNavLiElements = $('.' + config.dialogNavigationListItemClass); - }); + let scrollNavLiElements = $('.' + config.dialogNavigationListItemClass); let scrollspyElement = $('#' + config.mapManualScrollspyId); let whileScrolling = function(){ if(disableOnScrollEvent === false){ - for(let i = 0; i < scrolLBreakpointElements.length; i++){ - let offset = $(scrolLBreakpointElements[i]).offset().top; + for(let i = 0; i < scrollBreakpointElements.length; i++){ + let offset = $(scrollBreakpointElements[i]).offset().top; if( (offset - modalOffsetTop) > 0){ diff --git a/js/app/ui/dialog/map_info.js b/js/app/ui/dialog/map_info.js index 8e5c717a..473110e7 100644 --- a/js/app/ui/dialog/map_info.js +++ b/js/app/ui/dialog/map_info.js @@ -307,7 +307,7 @@ define([ createdCell: function(cell, cellData, rowData, rowIndex, colIndex){ // select system $(cell).on('click', function(e){ - Util.getMapModule().getActiveMap().triggerMenuEvent('SelectSystem', {systemId: rowData.id}); + Util.triggerMenuAction(Util.getMapModule().getActiveMap(), 'SelectSystem', {systemId: rowData.id}); }); } },{ @@ -382,9 +382,7 @@ define([ let statics = []; for(let wormholeName of cellData){ let wormholeData = Object.assign({}, Init.wormholes[wormholeName]); - let security = wormholeData.security; - let secClass = Util.getSecurityClassForSystem(security); - statics.push('' + security + ''); + statics.push('' + wormholeData.security + ''); } return statics.join('  '); } @@ -566,7 +564,7 @@ define([ createdCell: function(cell, cellData, rowData, rowIndex, colIndex){ // select system $(cell).on('click', function(e){ - Util.getMapModule().getActiveMap().triggerMenuEvent('SelectSystem', {systemId: rowData.source}); + Util.triggerMenuAction(Util.getMapModule().getActiveMap(), 'SelectSystem', {systemId: rowData.source}); }); } },{ @@ -596,7 +594,7 @@ define([ display: (cellData, type, rowData, meta) => { let connectionClasses = MapUtil.getConnectionFakeClassesByTypes(cellData); connectionClasses = connectionClasses.join(' '); - return '
    '; + return '
    '; } } },{ @@ -622,7 +620,7 @@ define([ createdCell: function(cell, cellData, rowData, rowIndex, colIndex){ // select system $(cell).on('click', function(e){ - Util.getMapModule().getActiveMap().triggerMenuEvent('SelectSystem', {systemId: rowData.target}); + Util.triggerMenuAction(Util.getMapModule().getActiveMap(), 'SelectSystem', {systemId: rowData.target}); }); } },{ @@ -812,8 +810,8 @@ define([ }, createdCell: function(cell, cellData, rowData, rowIndex, colIndex){ // open character information window (ingame) - $(cell).on('click', { tableApi: this.DataTable() }, function(e){ - let rowData = e.data.tableApi.row(this).data(); + $(cell).on('click', { tableApi: this.api(), rowIndex: rowIndex }, function(e){ + let rowData = e.data.tableApi.row(e.data.rowIndex).data(); Util.openIngameWindow(rowData.id); }); } @@ -852,7 +850,7 @@ define([ }, createdCell: function(cell, cellData, rowData, rowIndex, colIndex){ // open character information window (ingame) - $(cell).on('click', { tableApi: this.DataTable() }, function(e){ + $(cell).on('click', { tableApi: this.api() }, function(e){ let cellData = e.data.tableApi.cell(this).data(); Util.openIngameWindow(cellData.id); }); @@ -1134,8 +1132,8 @@ define([ }, createdCell: function(cell, cellData, rowData, rowIndex, colIndex){ // open character information window (ingame) - $(cell).on('click', { tableApi: this.DataTable() }, function(e){ - let rowData = e.data.tableApi.row(this).data(); + $(cell).on('click', { tableApi: this.api(), rowIndex: rowIndex }, function(e){ + let rowData = e.data.tableApi.row(e.data.rowIndex).data(); Util.openIngameWindow(rowData.context.data.character.id); }); } @@ -1260,7 +1258,7 @@ define([ ] } ); - logDataTable.buttons().container().appendTo( $(this).find('.' + config.tableToolsClass)); + logDataTable.buttons().container().appendTo($(this).find('.' + config.tableToolsClass)); }; /** diff --git a/js/app/ui/dialog/map_settings.js b/js/app/ui/dialog/map_settings.js index bd02f526..4e628d5c 100644 --- a/js/app/ui/dialog/map_settings.js +++ b/js/app/ui/dialog/map_settings.js @@ -35,6 +35,7 @@ define([ deleteExpiredConnectionsId: 'pf-map-dialog-delete-connections-expired', // id for "deleteExpiredConnections" checkbox deleteEolConnectionsId: 'pf-map-dialog-delete-connections-eol', // id for "deleteEOLConnections" checkbox persistentAliasesId: 'pf-map-dialog-persistent-aliases', // id for "persistentAliases" checkbox + persistentSignaturesId: 'pf-map-dialog-persistent-signatures', // id for "persistentSignatures" checkbox logHistoryId: 'pf-map-dialog-history', // id for "history logging" checkbox logActivityId: 'pf-map-dialog-activity', // id for "activity" checkbox @@ -157,6 +158,7 @@ define([ let deleteExpiredConnections = true; let deleteEolConnections = true; let persistentAliases = true; + let persistentSignatures = true; let logActivity = true; let logHistory = true; @@ -194,6 +196,7 @@ define([ deleteExpiredConnections = mapData.config.deleteExpiredConnections; deleteEolConnections = mapData.config.deleteEolConnections; persistentAliases = mapData.config.persistentAliases; + persistentSignatures = mapData.config.persistentSignatures; logActivity = mapData.config.logging.activity; logHistory = mapData.config.logging.history; @@ -251,9 +254,11 @@ define([ deleteExpiredConnectionsId : config.deleteExpiredConnectionsId, deleteEolConnectionsId : config.deleteEolConnectionsId, persistentAliasesId : config.persistentAliasesId, + persistentSignaturesId : config.persistentSignaturesId, deleteExpiredConnections: deleteExpiredConnections, deleteEolConnections: deleteEolConnections, persistentAliases: persistentAliases, + persistentSignatures: persistentSignatures, logHistoryId: config.logHistoryId, logActivityId: config.logActivityId, @@ -390,8 +395,8 @@ define([ if( form.find('#' + config.persistentAliasesId).length ){ formData.persistentAliases = formData.hasOwnProperty('persistentAliases') ? parseInt( formData.persistentAliases ) : 0; } - if( form.find('#' + config.persistentAliasesId).length ){ - formData.persistentAliases = formData.hasOwnProperty('persistentAliases') ? parseInt( formData.persistentAliases ) : 0; + if( form.find('#' + config.persistentSignaturesId).length ){ + formData.persistentSignatures = formData.hasOwnProperty('persistentSignatures') ? parseInt( formData.persistentSignatures ) : 0; } if( form.find('#' + config.logHistoryId).length ){ formData.logHistory = formData.hasOwnProperty('logHistory') ? parseInt( formData.logHistory ) : 0; @@ -422,7 +427,7 @@ define([ } $(mapInfoDialog).modal('hide'); - $(document).trigger('pf:closeMenu', [{}]); + Util.triggerMenuAction(document, 'Close'); } }).fail(function(jqXHR, status, error){ let reason = status + ' ' + error; diff --git a/js/app/ui/dialog/notification.js b/js/app/ui/dialog/notification.js index ba65c526..bae8efc0 100644 --- a/js/app/ui/dialog/notification.js +++ b/js/app/ui/dialog/notification.js @@ -6,9 +6,8 @@ define([ 'jquery', 'app/init', 'app/util', - 'app/render', 'bootbox' -], function($, Init, Util, Render, bootbox){ +], ($, Init, Util, bootbox) => { 'use strict'; diff --git a/js/app/ui/dialog/shortcuts.js b/js/app/ui/dialog/shortcuts.js index fd14e97e..9fd869c0 100644 --- a/js/app/ui/dialog/shortcuts.js +++ b/js/app/ui/dialog/shortcuts.js @@ -6,10 +6,9 @@ define([ 'jquery', 'app/init', 'app/util', - 'app/render', 'bootbox', 'app/key', -], function($, Init, Util, Render, bootbox, Key){ +], function($, Init, Util, bootbox, Key){ 'use strict'; diff --git a/js/app/ui/dialog/stats.js b/js/app/ui/dialog/stats.js index 6e3f2a8a..b136109d 100644 --- a/js/app/ui/dialog/stats.js +++ b/js/app/ui/dialog/stats.js @@ -10,7 +10,7 @@ define([ 'app/render', 'bootbox', 'peityInlineChart' -], function($, Init, Util, Render, bootbox){ +], ($, Init, Util, Render, bootbox) => { 'use strict'; let config = { @@ -83,12 +83,14 @@ define([ buttons: [ { extend: 'copy', + tag: 'a', className: config.moduleHeadlineIconClass, text: ' copy', exportOptions: { orthogonal: 'filter' } }, { extend: 'csv', + tag: 'a', className: config.moduleHeadlineIconClass, text: ' csv', exportOptions: { orthogonal: 'filter' } diff --git a/js/app/ui/dialog/system_effects.js b/js/app/ui/dialog/system_effects.js index de19846c..8d2b03a5 100644 --- a/js/app/ui/dialog/system_effects.js +++ b/js/app/ui/dialog/system_effects.js @@ -6,10 +6,9 @@ define([ 'jquery', 'app/init', 'app/util', - 'app/render', 'bootbox', 'app/map/util' -], ($, Init, Util, Render, bootbox, MapUtil) => { +], ($, Init, Util, bootbox, MapUtil) => { 'use strict'; let config = { @@ -57,7 +56,7 @@ define([ for(let [areaId, areaData] of Object.entries(effectData)){ let systemType = 'C' + areaId; - let securityClass = Util.getSecurityClassForSystem( systemType ); + let securityClass = Util.getSecurityClassForSystem(systemType); if(areaId === '1'){ rows.push( $('
    ') ); diff --git a/js/app/ui/form_element.js b/js/app/ui/form_element.js index 29339704..312b837b 100644 --- a/js/app/ui/form_element.js +++ b/js/app/ui/form_element.js @@ -166,7 +166,7 @@ define([ if(type.includes('wh_critical')){ styleClass.push('pf-wh-critical'); } - if(type.includes('frigate')){ + if(type.includes('wh_jump_mass_s')){ styleClass.push('pf-wh-frig'); } } @@ -211,6 +211,53 @@ define([ ); }; + /** + * init a select element as "select2" for connection size types + * @param options + */ + $.fn.initConnectionSizeSelect = function(options){ + let selectElement = $(this); + + let defaultConfig = { + dropdownParent: selectElement.parents('.modal-body'), + minimumResultsForSearch: -1, + width: '100%', + maxSelectionLength: 1 + }; + options = $.extend({}, defaultConfig, options); + + let formatConnectionSizeResultData = data => { + if(data.loading) return data.text; + if(data.placeholder) return data.placeholder; + + let connectionClass = MapUtil.getConnectionInfo(data.text, 'cssClass'); + + let label = Util.getObjVal(Init.wormholeSizes, data.text + '.label') || '?'; + let text = Util.getObjVal(Init.wormholeSizes, data.text + '.text') || 'all'; + + let markup = '
    '; + markup += '
    '; + markup += ''; + markup += '
    '; + markup += '
    '; + markup += '
    '; + markup += '
    '; + markup += '
    '; + markup += text; + markup += '
    '; + markup += '
    '; + + return $(markup); + }; + + options.templateSelection = formatConnectionSizeResultData; + options.templateResult = formatConnectionSizeResultData; + + $.when( + selectElement.select2(options) + ); + }; + /** * init a sselect element as "select2" for "status" selection * @param options diff --git a/js/app/ui/demo_map.js b/js/app/ui/layout/demo_map.js similarity index 99% rename from js/app/ui/demo_map.js rename to js/app/ui/layout/demo_map.js index a2c1bd78..3803037b 100644 --- a/js/app/ui/demo_map.js +++ b/js/app/ui/layout/demo_map.js @@ -5,7 +5,7 @@ define([ 'jquery', 'lazylinepainter' -], function($){ +], ($) => { 'use strict'; diff --git a/js/app/ui/header.js b/js/app/ui/layout/header_login.js similarity index 100% rename from js/app/ui/header.js rename to js/app/ui/layout/header_login.js diff --git a/js/app/ui/logo.js b/js/app/ui/layout/logo.js similarity index 98% rename from js/app/ui/logo.js rename to js/app/ui/layout/logo.js index 193547ff..7d62b125 100644 --- a/js/app/ui/logo.js +++ b/js/app/ui/layout/logo.js @@ -59,7 +59,7 @@ define([ }; // load Logo svg - requirejs(['text!templates/ui/logo.html', 'mustache'], function(template, Mustache){ + requirejs(['text!templates/layout/logo.html', 'mustache'], function(template, Mustache){ let logoData = { staticLogoId: config.staticLogoId, logoPartTopRightClass: config.logoPartTopRightClass, diff --git a/js/app/ui/module/connection_info.js b/js/app/ui/module/connection_info.js index bcac90cb..b165155a 100644 --- a/js/app/ui/module/connection_info.js +++ b/js/app/ui/module/connection_info.js @@ -18,8 +18,6 @@ define([ moduleHeadClass: 'pf-module-head', // class for module header moduleHandlerClass: 'pf-module-handler-drag', // class for "drag" handler - headUserShipClass: 'pf-head-user-ship', // class for "user settings" link - // connection info module moduleTypeClass: 'pf-connection-info-module', // class for this module @@ -100,15 +98,13 @@ define([ * @returns {jQuery} */ let getConnectionElement = (mapId, connectionId) => { - let connectionElement = $('
    ', { + return $('
    ', { id: getConnectionElementId(connectionId), class: ['col-xs-12', 'col-sm-4', 'col-lg-3' , config.connectionInfoPanelClass].join(' ') }).data({ mapId: mapId, connectionId: connectionId }); - - return connectionElement; }; /** @@ -116,13 +112,11 @@ define([ * @param mapId * @returns {void|jQuery|*} */ - let getInfoPanelControl = (mapId) => { - let connectionElement = getConnectionElement(mapId, 0).append($('
    ', { + let getInfoPanelControl = mapId => { + return getConnectionElement(mapId, 0).append($('
    ', { class: [Util.config.dynamicAreaClass, config.controlAreaClass].join(' '), html: ' add connection  ctrl + click' })); - - return connectionElement; }; /** @@ -168,7 +162,7 @@ define([ class: 'pf-link', html: connectionData.sourceAlias + '  ' }).on('click', function(){ - Util.getMapModule().getActiveMap().triggerMenuEvent('SelectSystem', {systemId: connectionData.source}); + Util.triggerMenuAction(Util.getMapModule().getActiveMap(), 'SelectSystem', {systemId: connectionData.source}); }), $('', { class: [config.connectionInfoTableLabelSourceClass].join(' ') @@ -183,7 +177,7 @@ define([ class: 'pf-link', html: '  ' + connectionData.targetAlias }).on('click', function(){ - Util.getMapModule().getActiveMap().triggerMenuEvent('SelectSystem', {systemId: connectionData.target}); + Util.triggerMenuAction(Util.getMapModule().getActiveMap(), 'SelectSystem', {systemId: connectionData.target}); }) ) ) @@ -306,22 +300,22 @@ define([ let connection = $().getConnectionById(data.mapId, data.connectionId); let signatureTypeNames = MapUtil.getConnectionDataFromSignatures(connection, connectionData); - let sourceLabel = signatureTypeNames.sourceLabels; - let targetLabel = signatureTypeNames.targetLabels; - sourceLabelElement.html(MapUtil.getEndpointOverlayContent(sourceLabel.join(', '))); - targetLabelElement.html(MapUtil.getEndpointOverlayContent(targetLabel.join(', '))); + let sourceLabels = signatureTypeNames.source.labels; + let targetLabels = signatureTypeNames.target.labels; + sourceLabelElement.html(MapUtil.formatEndpointOverlaySignatureLabel(sourceLabels)); + targetLabelElement.html(MapUtil.formatEndpointOverlaySignatureLabel(targetLabels)); // remove K162 - sourceLabel = sourceLabel.diff(['K162']); - targetLabel = targetLabel.diff(['K162']); + sourceLabels = sourceLabels.diff(['K162']); + targetLabels = targetLabels.diff(['K162']); // get static wormhole data by endpoint Labels let wormholeName = ''; let wormholeData = null; - if(sourceLabel.length === 1 && targetLabel.length === 0){ - wormholeName = sourceLabel[0]; - }else if(sourceLabel.length === 0 && targetLabel.length === 1){ - wormholeName = targetLabel[0]; + if(sourceLabels.length === 1 && targetLabels.length === 0){ + wormholeName = sourceLabels[0]; + }else if(sourceLabels.length === 0 && targetLabels.length === 1){ + wormholeName = targetLabels[0]; } if( @@ -329,7 +323,6 @@ define([ Init.wormholes.hasOwnProperty(wormholeName) ){ wormholeData = Object.assign({}, Init.wormholes[wormholeName]); - wormholeData.class = Util.getSecurityClassForSystem(wormholeData.security); // init wormhole tooltip ---------------------------------------------- let massTotalTooltipCell = tableElement.find('.' + config.connectionInfoTableCellMassTotalTooltipClass); @@ -416,7 +409,7 @@ define([ // get current ship data ---------------------------------------------------------- massShipCell.parent().toggle(showShip); if(showShip){ - shipData = $('.' + config.headUserShipClass).data('shipData'); + shipData = Util.getObjVal(Util.getCurrentCharacterLog(), 'ship'); if(shipData){ if(shipData.mass){ massShip = parseInt(shipData.mass); @@ -507,18 +500,14 @@ define([ * @param connectionId * @returns {string} */ - let getConnectionElementId = (connectionId) => { - return config.connectionInfoPanelId + connectionId; - }; + let getConnectionElementId = connectionId => config.connectionInfoPanelId + connectionId; /** * get all visible connection panel elements * @param moduleElement * @returns {*|T|{}} */ - let getConnectionElements = (moduleElement) => { - return moduleElement.find('.' + config.connectionInfoPanelClass).not('#' + getConnectionElementId(0)); - }; + let getConnectionElements = moduleElement => moduleElement.find('.' + config.connectionInfoPanelClass).not('#' + getConnectionElementId(0)); /** * enrich connectionData with "logs" data (if available) and other "missing" data @@ -703,6 +692,7 @@ define([ buttons: [ { name: 'addLog', + tag: 'a', className: config.moduleHeadlineIconClass, text: '', action: function(e, tableApi, node, conf){ @@ -1108,7 +1098,7 @@ define([ selectElementType.initUniverseTypeSelect({ categoryIds: [6], maxSelectionLength: 1, - selected: [Util.getObjVal(logData, 'ship.id')] + selected: [Util.getObjVal(logData, 'ship.typeId')] }).on('select2:select select2:unselecting', function(e){ // get ship mass from selected ship type and update mass input field let shipMass = e.params.data ? e.params.data.mass / 1000 : ''; diff --git a/js/app/ui/module/system_info.js b/js/app/ui/module/system_info.js index a59899de..5136c3bb 100644 --- a/js/app/ui/module/system_info.js +++ b/js/app/ui/module/system_info.js @@ -143,7 +143,6 @@ define([ ){ for(let wormholeName of systemData.statics){ let wormholeData = Object.assign({}, Init.wormholes[wormholeName]); - wormholeData.class = Util.getSecurityClassForSystem(wormholeData.security); staticsData.push(wormholeData); } } diff --git a/js/app/ui/module/system_intel.js b/js/app/ui/module/system_intel.js index 8ae99f7e..8c2f7631 100644 --- a/js/app/ui/module/system_intel.js +++ b/js/app/ui/module/system_intel.js @@ -53,7 +53,7 @@ define([ * @param statusData * @returns {string} */ - let getStatusData = (statusData) => { + let getStatusData = statusData => { return ''; }; @@ -430,7 +430,7 @@ define([ name: 'status', title: '', width: 2, - class: 'text-center', + className: ['text-center', 'all'].join(' '), data: 'status', render: { display: data => getStatusData(data), @@ -445,7 +445,7 @@ define([ title: '', width: 26, orderable: false, - className: [config.tableCellImageClass, 'text-center'].join(' '), + className: [config.tableCellImageClass, 'text-center', 'all'].join(' '), data: 'structure.id', defaultContent: '', render: { @@ -462,7 +462,7 @@ define([ name: 'structureType', title: 'type', width: 30, - className: [config.tableCellEllipsisClass].join(' '), + className: [config.tableCellEllipsisClass, 'all'].join(' '), data: 'structure.name', defaultContent: '', },{ @@ -470,7 +470,7 @@ define([ name: 'name', title: 'name', width: 60, - className: [config.tableCellEllipsisClass].join(' '), + className: [config.tableCellEllipsisClass, 'all'].join(' '), data: 'name' },{ targets: 4, @@ -478,7 +478,7 @@ define([ title: '', width: 26, orderable: false, - className: [config.tableCellImageClass, 'text-center'].join(' '), + className: [config.tableCellImageClass, 'text-center', 'all'].join(' '), data: 'owner.id', defaultContent: '', render: { @@ -497,14 +497,14 @@ define([ name: 'ownerName', title: 'owner', width: 50, - className: [config.tableCellEllipsisClass].join(' '), + className: [config.tableCellEllipsisClass, 'all'].join(' '), data: 'owner.name', defaultContent: '', },{ targets: 6, name: 'note', title: 'note', - className: [config.tableCellEllipsisClass].join(' '), + className: [config.tableCellEllipsisClass, 'all'].join(' '), data: 'description' },{ targets: 7, @@ -519,7 +519,7 @@ define([ title: '', orderable: false, width: 10, - class: ['text-center', config.dataTableActionCellClass, config.moduleHeadlineIconClass].join(' '), + className: ['text-center', config.dataTableActionCellClass, config.moduleHeadlineIconClass, 'all'].join(' '), data: null, render: { display: data => { @@ -550,7 +550,7 @@ define([ title: '', orderable: false, width: 10, - class: ['text-center', config.dataTableActionCellClass].join(' '), + className: ['text-center', config.dataTableActionCellClass, 'all'].join(' '), data: null, render: { display: data => { @@ -607,6 +607,7 @@ define([ },{ targets: 10, name: 'corporation', + className: 'never', // never show this column. see: https://datatables.net/extensions/responsive/classes data: 'corporation', visible: false, render: { diff --git a/js/app/ui/module/system_route.js b/js/app/ui/module/system_route.js index d65add0a..0afd3b9d 100644 --- a/js/app/ui/module/system_route.js +++ b/js/app/ui/module/system_route.js @@ -34,7 +34,9 @@ define([ routeDialogId: 'pf-route-dialog', // id for route "search" dialog systemDialogSelectClass: 'pf-system-dialog-select', // class for system select Element systemInfoRoutesTableClass: 'pf-system-route-table', // class for route tables - mapSelectId: 'pf-route-dialog-map-select', // id for "map" select + + routeDialogMapSelectId: 'pf-route-dialog-map-select', // id for "map" select + routeDialogSizeSelectId: 'pf-route-dialog-size-select', // id for "wh size" select dataTableActionCellClass: 'pf-table-action-cell', // class for "action" cells dataTableRouteCellClass: 'pf-table-route-cell', // class for "route" cells @@ -207,8 +209,8 @@ define([ }; let routeData = []; - dataTable.rows().every( function(){ - routeData.push( getRouteRequestDataFromRowData(this.data())); + dataTable.rows().every(function(){ + routeData.push(getRouteRequestDataFromRowData(this.data())); }); getRouteData({routeData: routeData}, context, callbackAddRouteRows); @@ -219,7 +221,7 @@ define([ * @param {Object} rowData * @returns {Object} */ - let getRouteRequestDataFromRowData = (rowData) => { + let getRouteRequestDataFromRowData = rowData => { return { mapIds: (rowData.hasOwnProperty('mapIds')) ? rowData.mapIds : [], systemFromData: (rowData.hasOwnProperty('systemFromData')) ? rowData.systemFromData : {}, @@ -230,8 +232,9 @@ define([ wormholes: (rowData.hasOwnProperty('wormholes')) ? rowData.wormholes | 0 : 1, wormholesReduced: (rowData.hasOwnProperty('wormholesReduced')) ? rowData.wormholesReduced | 0 : 1, wormholesCritical: (rowData.hasOwnProperty('wormholesCritical')) ? rowData.wormholesCritical | 0 : 1, - wormholesFrigate: (rowData.hasOwnProperty('wormholesFrigate')) ? rowData.wormholesFrigate | 0 : 1, wormholesEOL: (rowData.hasOwnProperty('wormholesEOL')) ? rowData.wormholesEOL | 0 : 1, + wormholesSizeMin: (rowData.hasOwnProperty('wormholesSizeMin')) ? rowData.wormholesSizeMin : '', + excludeTypes: (rowData.hasOwnProperty('excludeTypes')) ? rowData.excludeTypes : [], endpointsBubble: (rowData.hasOwnProperty('endpointsBubble')) ? rowData.endpointsBubble | 0 : 1, connections: (rowData.hasOwnProperty('connections')) ? rowData.connections.value | 0 : 0, flag: (rowData.hasOwnProperty('flag')) ? rowData.flag.value : 'shortest' @@ -255,13 +258,25 @@ define([ }); } } + + let sizeOptions = MapUtil.allConnectionJumpMassTypes().map(type => { + return { + id: type, + name: type, + selected: false + }; + }); + let data = { id: config.routeDialogId, + select2Class: Util.config.select2Class, selectClass: config.systemDialogSelectClass, - mapSelectId: config.mapSelectId, + routeDialogMapSelectId: config.routeDialogMapSelectId, + routeDialogSizeSelectId: config.routeDialogSizeSelectId, systemFromData: dialogData.systemFromData, systemToData: dialogData.systemToData, - mapSelectOptions: mapSelectOptions + mapSelectOptions: mapSelectOptions, + sizeOptions: sizeOptions }; requirejs(['text!templates/dialog/route.html', 'mustache'], (template, Mustache) => { @@ -300,6 +315,7 @@ define([ } // get all system data from select2 + // -> we could also get value from "routeDialogData" var, but we need systemName also let systemSelectData = form.find('.' + config.systemDialogSelectClass).select2('data'); if( @@ -324,8 +340,9 @@ define([ wormholes: routeDialogData.hasOwnProperty('wormholes') ? parseInt(routeDialogData.wormholes) : 0, wormholesReduced: routeDialogData.hasOwnProperty('wormholesReduced') ? parseInt(routeDialogData.wormholesReduced) : 0, wormholesCritical: routeDialogData.hasOwnProperty('wormholesCritical') ? parseInt(routeDialogData.wormholesCritical) : 0, - wormholesFrigate: routeDialogData.hasOwnProperty('wormholesFrigate') ? parseInt(routeDialogData.wormholesFrigate) : 0, wormholesEOL: routeDialogData.hasOwnProperty('wormholesEOL') ? parseInt(routeDialogData.wormholesEOL) : 0, + wormholesSizeMin: routeDialogData.wormholesSizeMin || '', + excludeTypes: getLowerSizeConnectionTypes(routeDialogData.wormholesSizeMin), endpointsBubble: routeDialogData.hasOwnProperty('endpointsBubble') ? parseInt(routeDialogData.endpointsBubble) : 0 }] }; @@ -343,15 +360,18 @@ define([ // init some dialog/form observer setDialogObserver( $(this) ); - // init map select ---------------------------------------------------------------- - let mapSelect = $(this).find('#' + config.mapSelectId); + // init map select ------------------------------------------------------------------------------------ + let mapSelect = findRouteDialog.find('#' + config.routeDialogMapSelectId); mapSelect.initMapSelect(); + + // init connection jump size select ------------------------------------------------------------------- + findRouteDialog.find('#' + config.routeDialogSizeSelectId).initConnectionSizeSelect(); }); findRouteDialog.on('shown.bs.modal', function(e){ - // init system select live search ------------------------------------------------ + // init system select live search -------------------------------------------------------------------- // -> add some delay until modal transition has finished let systemTargetSelect = $(this).find('.' + config.systemDialogSelectClass); systemTargetSelect.delay(240).initSystemSelect({key: 'id'}); @@ -399,7 +419,7 @@ define([ skipSearch: requestRouteData.length >= defaultRoutesCount }; - requestRouteData.push( getRouteRequestDataFromRowData( searchData )); + requestRouteData.push(getRouteRequestDataFromRowData(searchData)); } } } @@ -493,7 +513,7 @@ define([ settingsDialog.on('shown.bs.modal', function(e){ - // init default system select ----------------------------------------------------- + // init default system select --------------------------------------------------------------------- // -> add some delay until modal transition has finished let systemTargetSelect = $(this).find('.' + config.systemDialogSelectClass); systemTargetSelect.delay(240).initSystemSelect({key: 'id', maxSelectionLength: maxSelectionLength}); @@ -532,14 +552,13 @@ define([ let wormholeCheckbox = routeDialog.find('input[type="checkbox"][name="wormholes"]'); let wormholeReducedCheckbox = routeDialog.find('input[type="checkbox"][name="wormholesReduced"]'); let wormholeCriticalCheckbox = routeDialog.find('input[type="checkbox"][name="wormholesCritical"]'); - let wormholeFrigateCheckbox = routeDialog.find('input[type="checkbox"][name="wormholesFrigate"]'); let wormholeEolCheckbox = routeDialog.find('input[type="checkbox"][name="wormholesEOL"]'); + let wormholeSizeSelect = routeDialog.find('#' + config.routeDialogSizeSelectId); // store current "checked" state for each box --------------------------------------------- let storeCheckboxStatus = function(){ wormholeReducedCheckbox.data('selectState', wormholeReducedCheckbox.prop('checked')); wormholeCriticalCheckbox.data('selectState', wormholeCriticalCheckbox.prop('checked')); - wormholeFrigateCheckbox.data('selectState', wormholeFrigateCheckbox.prop('checked')); wormholeEolCheckbox.data('selectState', wormholeEolCheckbox.prop('checked')); }; @@ -547,24 +566,24 @@ define([ let onWormholeCheckboxChange = function(){ if( $(this).is(':checked') ){ + wormholeSizeSelect.prop('disabled', false); + wormholeReducedCheckbox.prop('disabled', false); wormholeCriticalCheckbox.prop('disabled', false); - wormholeFrigateCheckbox.prop('disabled', false); wormholeEolCheckbox.prop('disabled', false); wormholeReducedCheckbox.prop('checked', wormholeReducedCheckbox.data('selectState')); wormholeCriticalCheckbox.prop('checked', wormholeCriticalCheckbox.data('selectState')); - wormholeFrigateCheckbox.prop('checked', wormholeFrigateCheckbox.data('selectState')); wormholeEolCheckbox.prop('checked', wormholeEolCheckbox.data('selectState')); }else{ + wormholeSizeSelect.prop('disabled', true); + storeCheckboxStatus(); wormholeReducedCheckbox.prop('checked', false); wormholeReducedCheckbox.prop('disabled', true); wormholeCriticalCheckbox.prop('checked', false); wormholeCriticalCheckbox.prop('disabled', true); - wormholeFrigateCheckbox.prop('checked', false); - wormholeFrigateCheckbox.prop('disabled', true); wormholeEolCheckbox.prop('checked', false); wormholeEolCheckbox.prop('disabled', true); } @@ -760,8 +779,9 @@ define([ wormholes: routeData.wormholes, wormholesReduced: routeData.wormholesReduced, wormholesCritical: routeData.wormholesCritical, - wormholesFrigate: routeData.wormholesFrigate, wormholesEOL: routeData.wormholesEOL, + wormholesSizeMin: routeData.wormholesSizeMin, + excludeTypes: routeData.excludeTypes, endpointsBubble: routeData.endpointsBubble, connections: { value: 0, @@ -823,7 +843,7 @@ define([ // check for wormhole let icon = 'fas fa-square'; if( /^J\d+$/.test(systemName) ){ - icon = 'far fa-dot-circle'; + icon = 'fas fa-dot-circle'; } let system = ' { + let lowerSizeTypes = []; + let jumpMassMin = Util.getObjVal(Init.wormholeSizes, connectionType + '.jumpMassMin') || 0; + + if(jumpMassMin){ + for(let [type, data] of Object.entries(Init.wormholeSizes)){ + if(data.jumpMassMin < jumpMassMin){ + lowerSizeTypes.push(type); + } + } + } + + return lowerSizeTypes; + }; + return { config: config, getModule: getModule, diff --git a/js/app/ui/module/system_signature.js b/js/app/ui/module/system_signature.js index 1a9c4870..3db47abd 100644 --- a/js/app/ui/module/system_signature.js +++ b/js/app/ui/module/system_signature.js @@ -68,7 +68,7 @@ define([ 'Kosmische Anomalie', // de: "Cosmic Anomaly" 'Kosmische Signatur', // de: "Cosmic Signature" 'Космическая аномалия', // ru: "Cosmic Anomaly" - 'Скрытый сигнал', // rm: "Cosmic Signature" + 'Скрытый сигнал', // ru: "Cosmic Signature" 'Anomalie cosmique', // fr: "Cosmic Anomaly" 'Signature cosmique', // fr: "Cosmic Signature" '宇宙の特異点', // ja: "Cosmic Anomaly" @@ -2186,21 +2186,25 @@ define([ buttons: [ { name: 'filterGroup', + tag: 'a', className: config.moduleHeadlineIconClass, text: '' // set by js (xEditable) }, { name: 'undo', + tag: 'a', className: config.moduleHeadlineIconClass, text: '' // set by js (xEditable) }, { name: 'selectAll', + tag: 'a', className: config.moduleHeadlineIconClass, text: 'select all' }, { name: 'delete', + tag: 'a', className: [config.moduleHeadlineIconClass, config.sigTableClearButtonClass].join(' '), text: 'delete (0)' } @@ -2351,8 +2355,7 @@ define([ // "lazy update" toggle --------------------------------------------------------------------------------------- moduleElement.find('.' + config.moduleHeadlineIconLazyClass).on('click', function(e){ - let button = $(this); - button.toggleClass('active'); + $(this).toggleClass('active'); }); // set multi row select --------------------------------------------------------------------------------------- @@ -2383,31 +2386,22 @@ define([ // event listener for global "paste" signatures into the page ------------------------------------------------- moduleElement.on('pf:updateSystemSignatureModuleByClipboard', {tableApi: primaryTableApi}, function(e, clipboard){ + let lazyUpdateToggle = moduleElement.find('.' + config.moduleHeadlineIconLazyClass); let signatureOptions = { - deleteOld: moduleElement.find('.' + config.moduleHeadlineIconLazyClass).hasClass('active') ? 1 : 0 + deleteOld: lazyUpdateToggle.hasClass('active') ? 1 : 0 }; + + // "disable" lazy update icon -> prevents accidental removal for next paste #724 + lazyUpdateToggle.toggleClass('active', false); + updateSignatureTableByClipboard(e.data.tableApi, systemData, clipboard, signatureOptions); }); // signature column - "type" popover -------------------------------------------------------------------------- - moduleElement.find('.' + config.sigTableClass).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: 'top', - show: true - }); - } - }, - out: function(e){ - $(this).destroyPopover(); - }, - selector: '.editable-click:not(.editable-open) span[class^="pf-system-sec-"]' - }); + MapUtil.initWormholeInfoTooltip( + moduleElement.find('.' + config.sigTableClass), + '.editable-click:not(.editable-open) span[class^="pf-system-sec-"]' + ); // signature column - "info" popover -------------------------------------------------------------------------- moduleElement.find('.' + config.sigTablePrimaryClass).hoverIntent({ diff --git a/js/app/util.js b/js/app/util.js index b49c270d..778d5b47 100644 --- a/js/app/util.js +++ b/js/app/util.js @@ -38,7 +38,7 @@ define([ // head headMapTrackingId: 'pf-head-map-tracking', // id for "map tracking" toggle (checkbox) - headCurrentLocationId: 'pf-head-current-location', // id for "show current location" element + headUserLocationId: 'pf-head-user-location', // id for "location" breadcrumb // menu menuButtonFullScreenId: 'pf-menu-button-fullscreen', // id for menu button "fullScreen" @@ -399,7 +399,7 @@ define([ }; /** - * check multiple element if they arecurrently visible in viewport + * check multiple element if they are currently visible in viewport * @returns {Array} */ $.fn.isInViewport = function(){ @@ -963,11 +963,35 @@ define([ * init utility prototype functions */ let initPrototypes = () => { - // Array diff - // [1,2,3,4,5,6].diff( [3,4,5] ); - // => [1, 2, 6] + + /** + * Array diff + * [1,2,3,4,5].diff([4,5,6]) => [1,2,3] + * @param a + * @returns {*[]} + */ Array.prototype.diff = function(a){ - return this.filter(function(i){return a.indexOf(i) < 0;}); + return this.filter(i => !a.includes(i)); + }; + + /** + * Array intersect + * [1,2,3,4,5].intersect([4,5,6]) => [4,5] + * @param a + * @returns {*[]} + */ + Array.prototype.intersect = function(a){ + return this.filter(i => a.includes(i)); + }; + + /** + * compares two arrays if all elements in a are also in b + * element order is ignored + * @param a + * @returns {boolean} + */ + Array.prototype.equalValues = function(a){ + return this.diff(a).concat(a.diff(this)).length === 0; }; /** @@ -1484,21 +1508,29 @@ define([ }; /** - * set currentUserData as "global" variable - * this function should be called continuously after data change - * to keep the data always up2data + * set currentUserData as "global" store var + * this function should be called whenever userData changed * @param userData + * @returns {boolean} true on success */ - let setCurrentUserData = (userData) => { - Init.currentUserData = userData; + let setCurrentUserData = userData => { + let isSet = false; - // check if function is available - // this is not the case in "login" page - if( $.fn.updateHeaderUserData ){ - $.fn.updateHeaderUserData(); + // check if userData is valid + if(userData && userData.character && userData.characters){ + let changes = compareUserData(getCurrentUserData(), userData); + // check if there is any change + if(Object.values(changes).some(val => val)){ + $(document).trigger('pf:changedUserData', [userData, changes]); + } + + Init.currentUserData = userData; + isSet = true; + }else{ + console.error('Could not set userData %o. Missing or malformed obj', userData); } - return getCurrentUserData(); + return isSet; }; /** @@ -1514,14 +1546,7 @@ define([ * @returns {number} */ let getCurrentCharacterId = () => { - let userData = getCurrentUserData(); - let currentCharacterId = 0; - if( - userData && - userData.character - ){ - currentCharacterId = parseInt( userData.character.id ); - } + let currentCharacterId = parseInt(getObjVal(getCurrentUserData(), 'character.id')) || 0; if(!currentCharacterId){ // no active character... -> get default characterId from initial page load @@ -1531,6 +1556,28 @@ define([ return currentCharacterId; }; + /** + * compares two userData objects for changes that are relevant + * @param oldUserData + * @param newUserData + * @returns {{characterShipType: *, charactersIds: boolean, characterLogLocation: *, characterSystemId: *, userId: *, characterId: *}} + */ + let compareUserData = (oldUserData, newUserData) => { + let valueChanged = key => getObjVal(oldUserData, key) !== getObjVal(newUserData, key); + + let oldCharactersIds = (getObjVal(oldUserData, 'characters') || []).map(data => data.id).sort(); + let newCharactersIds = (getObjVal(newUserData, 'characters') || []).map(data => data.id).sort(); + + return { + userId: valueChanged('id'), + characterId: valueChanged('character.id'), + characterLogLocation: valueChanged('character.logLocation'), + characterSystemId: valueChanged('character.log.system.id'), + characterShipType: valueChanged('character.log.ship.typeId'), + charactersIds: oldCharactersIds.toString() !== newCharactersIds.toString() + }; + }; + /** * get a unique ID for each tab * -> store ID in session storage @@ -1808,16 +1855,34 @@ define([ * @param jqXHR XMLHttpRequest instance * @returns {boolean} */ - let isXHRAborted = (jqXHR) => { + let isXHRAborted = jqXHR => { return !jqXHR.getAllResponseHeaders(); }; + /** + * trigger global menu action 'event' on dom 'element' with optional 'data' + * @param element + * @param action + * @param data + */ + let triggerMenuAction = (element, action, data) => { + if(element){ + if(typeof(action) === 'string' && action.length){ + $(element).trigger('pf:menuAction', [action, data]); + }else{ + console.error('Invalid action: %o', action); + } + }else{ + console.error('Invalid element: %o', element); + } + }; + /** * get label element for role data * @param role * @returns {*|jQuery|HTMLElement} */ - let getLabelByRole = (role) => { + let getLabelByRole = role => { return $('', { class: ['label', 'label-' + role.style].join(' '), text: role.label @@ -1829,7 +1894,7 @@ define([ * @param mapOverlay * @returns {jQuery} */ - let getMapElementFromOverlay = (mapOverlay) => { + let getMapElementFromOverlay = mapOverlay => { return $(mapOverlay).parents('.' + config.mapWrapperClass).find('.' + config.mapClass); }; @@ -2266,7 +2331,7 @@ define([ */ let getDataIndexByMapId = (data, mapId) => { let index = false; - if( Array.isArray(data) && mapId === parseInt(mapId, 10) ){ + if(Array.isArray(data) && mapId === parseInt(mapId, 10)){ for(let i = 0; i < data.length; i++){ if(data[i].config.id === mapId){ index = i; @@ -2330,9 +2395,7 @@ define([ * @param mapId * @returns {boolean|int} */ - let getCurrentMapUserDataIndex = mapId => { - return getDataIndexByMapId(Init.currentMapUserData, mapId); - }; + let getCurrentMapUserDataIndex = mapId => getDataIndexByMapId(Init.currentMapUserData, mapId); /** * update cached mapUserData for a single map @@ -2375,17 +2438,13 @@ define([ let getCurrentMapData = mapId => { let currentMapData = false; - if( mapId === parseInt(mapId, 10) ){ - // search for a specific map - for(let i = 0; i < Init.currentMapData.length; i++){ - if(Init.currentMapData[i].config.id === mapId){ - currentMapData = Init.currentMapData[i]; - break; - } + if(Init.currentMapData){ + if(mapId === parseInt(mapId, 10)){ + currentMapData = Init.currentMapData.find(mapData => mapData.config.id === mapId); + }else{ + // get data for all maps + currentMapData = Init.currentMapData; } - }else{ - // get data for all maps - currentMapData = Init.currentMapData; } return currentMapData; @@ -2405,7 +2464,7 @@ define([ * @param mapData */ let updateCurrentMapData = mapData => { - let mapDataIndex = getCurrentMapDataIndex( mapData.config.id ); + let mapDataIndex = getCurrentMapDataIndex(mapData.config.id); if(mapDataIndex !== false){ Init.currentMapData[mapDataIndex].config = mapData.config; @@ -2424,8 +2483,8 @@ define([ let filterCurrentMapData = (path, value) => { let currentMapData = getCurrentMapData(); if(currentMapData){ - currentMapData = currentMapData.filter((mapData) => { - return (getObjVal(mapData, path) === value); + currentMapData = currentMapData.filter(mapData => { + return getObjVal(mapData, path) === value; }); } return currentMapData; @@ -2436,7 +2495,7 @@ define([ * @param mapId */ let deleteCurrentMapData = mapId => { - Init.currentMapData = Init.currentMapData.filter((mapData) => { + Init.currentMapData = Init.currentMapData.filter(mapData => { return (mapData.config.id !== mapId); }); }; @@ -2445,20 +2504,7 @@ define([ * get the current log data for the current user character * @returns {boolean} */ - let getCurrentCharacterLog = () => { - let characterLog = false; - let currentUserData = getCurrentUserData(); - - if( - currentUserData && - currentUserData.character && - currentUserData.character.log - ){ - characterLog = currentUserData.character.log; - } - - return characterLog; - }; + let getCurrentCharacterLog = () => getObjVal(getCurrentUserData(), 'character.log') || false; /** * get information for the current mail user @@ -2793,28 +2839,16 @@ define([ return Init.currentSystemData; }; - /** - * set current location data - * -> system data where current user is located - * @param systemId - * @param systemName - */ - let setCurrentLocationData = (systemId, systemName) => { - let locationLink = $('#' + config.headCurrentLocationId).find('a'); - locationLink.data('systemId', systemId); - locationLink.data('systemName', systemName); - }; - /** * get current location data * -> system data where current user is located * @returns {{id: *, name: *}} */ let getCurrentLocationData = () => { - let locationLink = $('#' + config.headCurrentLocationId).find('a'); + let breadcrumbElement = $('#' + config.headUserLocationId + '>li:last-of-type'); return { - id: locationLink.data('systemId') || 0, - name: locationLink.data('systemName') || false + id: parseInt(breadcrumbElement.attr('data-systemId')) || 0, + name: breadcrumbElement.attr('data-systemName') || false }; }; @@ -3056,6 +3090,18 @@ define([ */ let isDomElement = obj => !!(obj && obj.nodeType === 1); + /** + * converts array of objects into object with properties + * @param array + * @param keyField + * @returns {*} + */ + let arrayToObject = (array, keyField = 'id') => + array.reduce((obj, item) => { + obj[item[keyField]] = item; + return obj; + }, {}); + /** * get deep json object value if exists * -> e.g. key = 'first.last.third' string @@ -3204,6 +3250,7 @@ define([ setSyncStatus: setSyncStatus, getSyncType: getSyncType, isXHRAborted: isXHRAborted, + triggerMenuAction: triggerMenuAction, getLabelByRole: getLabelByRole, getMapElementFromOverlay: getMapElementFromOverlay, getMapModule: getMapModule, @@ -3233,7 +3280,6 @@ define([ getCurrentCharacterId: getCurrentCharacterId, setCurrentSystemData: setCurrentSystemData, getCurrentSystemData: getCurrentSystemData, - setCurrentLocationData: setCurrentLocationData, getCurrentLocationData: getCurrentLocationData, getCurrentUserInfo: getCurrentUserInfo, getCurrentCharacterLog: getCurrentCharacterLog, @@ -3262,6 +3308,7 @@ define([ htmlDecode: htmlDecode, isValidHtml: isValidHtml, isDomElement: isDomElement, + arrayToObject: arrayToObject, getObjVal: getObjVal, redirect: redirect, logout: logout, diff --git a/js/app/worker/map.js b/js/app/worker/map.js index c34e95e1..53975bd6 100644 --- a/js/app/worker/map.js +++ b/js/app/worker/map.js @@ -12,14 +12,14 @@ let ports = []; let characterPorts = []; // init "WebSocket" connection ======================================================================================== -let initSocket = (uri) => { +let initSocket = uri => { let MsgWorkerOpen = new MsgWorker('ws:open'); if(socket === null){ socket = new WebSocket(uri); // "WebSocket" open ----------------------------------------------------------------------- - socket.onopen = (e) => { + socket.onopen = e => { MsgWorkerOpen.meta({ readyState: socket.readyState }); @@ -28,7 +28,7 @@ let initSocket = (uri) => { }; // "WebSocket message --------------------------------------------------------------------- - socket.onmessage = (e) => { + socket.onmessage = e => { let response = JSON.parse(e.data); let MsgWorkerSend = new MsgWorker('ws:send'); @@ -43,7 +43,7 @@ let initSocket = (uri) => { }; // "WebSocket" close ---------------------------------------------------------------------- - socket.onclose = (closeEvent) => { + socket.onclose = closeEvent => { let MsgWorkerClosed = new MsgWorker('ws:closed'); MsgWorkerClosed.meta({ readyState: socket.readyState, @@ -57,7 +57,7 @@ let initSocket = (uri) => { }; // "WebSocket" error ---------------------------------------------------------------------- - socket.onerror = (e) => { + socket.onerror = e => { let MsgWorkerError = new MsgWorker('ws:error'); MsgWorkerError.meta({ readyState: socket.readyState @@ -75,11 +75,11 @@ let initSocket = (uri) => { }; // send message to port(s) ============================================================================================ -let sendToCurrentPort = (load) => { +let sendToCurrentPort = load => { ports[ports.length - 1].postMessage(load); }; -let broadcastPorts = (load) => { +let broadcastPorts = load => { // default: sent to all ports let sentToPorts = ports; @@ -114,7 +114,7 @@ let addPort = (port, characterId) => { } }; -let getPortsByCharacterIds = (characterIds) => { +let getPortsByCharacterIds = characterIds => { let ports = []; for(let i = 0; i < characterPorts.length; i++){ @@ -128,8 +128,37 @@ let getPortsByCharacterIds = (characterIds) => { return ports; }; +/** + * + * @param port + * @returns {int[]} + */ +let removePort = port => { + let characterIds = []; + + // reverse loop required because of array index reset after splice() + let i = characterPorts.length; + while(i--){ + if(characterPorts[i].port === port){ + // collectt all character Ids mapped to the removed port + characterIds.push(characterPorts[i].characterId); + characterPorts.splice(i, 1); + } + } + + let j = ports.length; + while(j--){ + if(ports[j] === port){ + ports.splice(j, 1); + } + } + + // return unique characterIds + return [...new Set(characterIds)]; +}; + // "SharedWorker" connection ========================================================================================== -self.addEventListener('connect', (event) => { // jshint ignore:line +self.addEventListener('connect', event => { // jshint ignore:line let port = event.ports[0]; addPort(port); @@ -145,12 +174,28 @@ self.addEventListener('connect', (event) => { // jshint ignore:line initSocket(data.uri); break; case 'ws:send': - let MsgSocket = { + socket.send(JSON.stringify({ task: MsgWorkerMessage.task(), load: MsgWorkerMessage.data() - }; + })); + break; + case 'sw:closePort': + port.close(); - socket.send(JSON.stringify(MsgSocket)); + // remove port from store + // -> charIds managed by closed port + let characterIds = removePort(port); + + // check if there are still other ports active that manage removed ports + // .. if not -> send "unsubscribe" event to WebSocket server + let portsLeft = getPortsByCharacterIds(characterIds); + + if(!portsLeft.length){ + socket.send(JSON.stringify({ + task: MsgWorkerMessage.task(), + load: characterIds + })); + } break; case 'ws:close': // closeSocket(); diff --git a/js/lib/datatables/Buttons-1.2.1/js/buttons.html5.min.js b/js/lib/datatables/Buttons-1.2.1/js/buttons.html5.min.js deleted file mode 100644 index 3fc13418..00000000 --- a/js/lib/datatables/Buttons-1.2.1/js/buttons.html5.min.js +++ /dev/null @@ -1,26 +0,0 @@ -(function(g){"function"===typeof define&&define.amd?define(["jquery","datatables.net","datatables.net-buttons"],function(j){return g(j,window,document)}):"object"===typeof exports?module.exports=function(j,i,q,r){j||(j=window);if(!i||!i.fn.dataTable)i=require("datatables.net")(j,i).$;i.fn.dataTable.Buttons||require("datatables.net-buttons")(j,i);return g(i,j,j.document,q,r)}:g(jQuery,window,document)})(function(g,j,i,q,r,m){function E(a,b){v===m&&(v=-1===y.serializeToString(g.parseXML(F["xl/worksheets/sheet1.xml"])).indexOf("xmlns:r")); -g.each(b,function(b,c){if(g.isPlainObject(c)){var e=a.folder(b);E(e,c)}else{if(v){var e=c.childNodes[0],f,h,n=[];for(f=e.attributes.length-1;0<=f;f--){h=e.attributes[f].nodeName;var k=e.attributes[f].nodeValue;-1!==h.indexOf(":")&&(n.push({name:h,value:k}),e.removeAttribute(h))}f=0;for(h=n.length;f'+ -e),e=e.replace(/_dt_b_namespace_token_/g,":"));e=e.replace(//g,"");a.file(b,e)}})}function o(a,b,d){var c=a.createElement(b);d&&(d.attr&&g(c).attr(d.attr),d.children&&g.each(d.children,function(a,b){c.appendChild(b)}),d.text&&c.appendChild(a.createTextNode(d.text)));return c}function N(a,b){var d=a.header[b].length,c;a.footer&&a.footer[b].length>d&&(d=a.footer[b].length);for(var e=0,f=a.body.length;ed&& -(d=c),400&&(b=b+f);b=b+(e?e+(""+a[c]).replace(g,h+e)+e:a[c])}return b},j=b.header?k(c.header)+d:"",i=b.footer&&c.footer?d+k(c.footer):"",u=[],D=0,l=c.body.length;D', -"xl/_rels/workbook.xml.rels":'',"[Content_Types].xml":'', -"xl/workbook.xml":'', -"xl/worksheets/sheet1.xml":'',"xl/styles.xml":''}; -s.ext.buttons.copyHtml5={className:"buttons-copy buttons-html5",text:function(a){return a.i18n("buttons.copy","Copy")},action:function(a,b,d,c){var a=L(b,c),e=a.str,d=g("
    ").css({height:1,width:1,overflow:"hidden",position:"fixed",top:0,left:0});c.customize&&(e=c.customize(e,c));c=g("",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var ve=r.documentElement,ye=/^key/,be=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,we=/^([^.]*)(?:\.(.+)|)/;function xe(){return!0}function Ce(){return!1}function Se(){try{return r.activeElement}catch(e){}}function Te(e,t,n,r,o,a){var i,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Te(e,s,n,r,t[s],a);return e}if(null==r&&null==o?(o=n,r=n=void 0):null==o&&("string"==typeof n?(o=r,r=void 0):(o=r,r=n,n=void 0)),!1===o)o=Ce;else if(!o)return e;return 1===a&&(i=o,(o=function(e){return w().off(e),i.apply(this,arguments)}).guid=i.guid||(i.guid=w.guid++)),e.each(function(){w.event.add(this,t,o,r,n)})}w.event={global:{},add:function(e,t,n,r,o){var a,i,s,l,c,u,d,f,p,h,m,g=G.get(e);if(g)for(n.handler&&(n=(a=n).handler,o=a.selector),o&&w.find.matchesSelector(ve,o),n.guid||(n.guid=w.guid++),(l=g.events)||(l=g.events={}),(i=g.handle)||(i=g.handle=function(t){return void 0!==w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),c=(t=(t||"").match(L)||[""]).length;c--;)p=m=(s=we.exec(t[c])||[])[1],h=(s[2]||"").split(".").sort(),p&&(d=w.event.special[p]||{},p=(o?d.delegateType:d.bindType)||p,d=w.event.special[p]||{},u=w.extend({type:p,origType:m,data:r,handler:n,guid:n.guid,selector:o,needsContext:o&&w.expr.match.needsContext.test(o),namespace:h.join(".")},a),(f=l[p])||((f=l[p]=[]).delegateCount=0,d.setup&&!1!==d.setup.call(e,r,h,i)||e.addEventListener&&e.addEventListener(p,i)),d.add&&(d.add.call(e,u),u.handler.guid||(u.handler.guid=n.guid)),o?f.splice(f.delegateCount++,0,u):f.push(u),w.event.global[p]=!0)},remove:function(e,t,n,r,o){var a,i,s,l,c,u,d,f,p,h,m,g=G.hasData(e)&&G.get(e);if(g&&(l=g.events)){for(c=(t=(t||"").match(L)||[""]).length;c--;)if(p=m=(s=we.exec(t[c])||[])[1],h=(s[2]||"").split(".").sort(),p){for(d=w.event.special[p]||{},f=l[p=(r?d.delegateType:d.bindType)||p]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=a=f.length;a--;)u=f[a],!o&&m!==u.origType||n&&n.guid!==u.guid||s&&!s.test(u.namespace)||r&&r!==u.selector&&("**"!==r||!u.selector)||(f.splice(a,1),u.selector&&f.delegateCount--,d.remove&&d.remove.call(e,u));i&&!f.length&&(d.teardown&&!1!==d.teardown.call(e,h,g.handle)||w.removeEvent(e,p,g.handle),delete l[p])}else for(p in l)w.event.remove(e,p+t[c],n,r,!0);w.isEmptyObject(l)&&G.remove(e,"handle events")}},dispatch:function(e){var t,n,r,o,a,i,s=w.event.fix(e),l=new Array(arguments.length),c=(G.get(this,"events")||{})[s.type]||[],u=w.event.special[s.type]||{};for(l[0]=s,t=1;t=1))for(;c!==this;c=c.parentNode||this)if(1===c.nodeType&&("click"!==e.type||!0!==c.disabled)){for(a=[],i={},n=0;n-1:w.find(o,this,null,[c]).length),i[o]&&a.push(r);a.length&&s.push({elem:c,handlers:a})}return c=this,l\x20\t\r\n\f]*)[^>]*)\/>/gi,De=/\s*$/g;function ke(e,t){return I(e,"table")&&I(11!==t.nodeType?t:t.firstChild,"tr")&&w(e).children("tbody")[0]||e}function Oe(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Ee(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Pe(e,t){var n,r,o,a,i,s,l,c;if(1===t.nodeType){if(G.hasData(e)&&(a=G.access(e),i=G.set(t,a),c=a.events))for(o in delete i.handle,i.events={},c)for(n=0,r=c[o].length;n1&&"string"==typeof g&&!h.checkClone&&Ie.test(g))return e.each(function(o){var a=e.eq(o);v&&(t[0]=g.call(this,o,a.html())),Re(a,t,n,r)});if(f&&(a=(o=ge(t,e[0].ownerDocument,!1,e,r)).firstChild,1===o.childNodes.length&&(o=a),a||r)){for(l=(s=w.map(pe(o,"script"),Oe)).length;d")},clone:function(e,t,n){var r,o,a,i,s=e.cloneNode(!0),l=w.contains(e.ownerDocument,e);if(!(h.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||w.isXMLDoc(e)))for(i=pe(s),r=0,o=(a=pe(e)).length;r0&&he(i,!l&&pe(e,"script")),s},cleanData:function(e){for(var t,n,r,o=w.event.special,a=0;void 0!==(n=e[a]);a++)if(X(n)){if(t=n[G.expando]){if(t.events)for(r in t.events)o[r]?w.event.remove(n,r):w.removeEvent(n,r,t.handle);n[G.expando]=void 0}n[K.expando]&&(n[K.expando]=void 0)}}}),w.fn.extend({detach:function(e){return Le(this,e,!0)},remove:function(e){return Le(this,e)},text:function(e){return q(this,function(e){return void 0===e?w.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Re(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||ke(this,e).appendChild(e)})},prepend:function(){return Re(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=ke(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(w.cleanData(pe(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return w.clone(this,e,t)})},html:function(e){return q(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!De.test(e)&&!fe[(ue.exec(e)||["",""])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n=0&&(l+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-a-l-s-.5))),l}function Ke(e,t,n){var r=$e(e),o=Be(e,t,r),a="border-box"===w.css(e,"boxSizing",!1,r),i=a;if(Ne.test(o)){if(!n)return o;o="auto"}return i=i&&(h.boxSizingReliable()||o===e.style[t]),("auto"===o||!parseFloat(o)&&"inline"===w.css(e,"display",!1,r))&&(o=e["offset"+t[0].toUpperCase()+t.slice(1)],i=!0),(o=parseFloat(o)||0)+Ge(e,t,n||(a?"border":"content"),i,r,o)+"px"}function Qe(e,t,n,r,o){return new Qe.prototype.init(e,t,n,r,o)}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Be(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var o,a,i,s=V(t),l=qe.test(t),c=e.style;if(l||(t=Xe(s)),i=w.cssHooks[t]||w.cssHooks[s],void 0===n)return i&&"get"in i&&void 0!==(o=i.get(e,!1,r))?o:c[t];"string"==(a=typeof n)&&(o=te.exec(n))&&o[1]&&(n=ae(e,t,o),a="number"),null!=n&&n==n&&("number"===a&&(n+=o&&o[3]||(w.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==t.indexOf("background")||(c[t]="inherit"),i&&"set"in i&&void 0===(n=i.set(e,n,r))||(l?c.setProperty(t,n):c[t]=n))}},css:function(e,t,n,r){var o,a,i,s=V(t);return qe.test(t)||(t=Xe(s)),(i=w.cssHooks[t]||w.cssHooks[s])&&"get"in i&&(o=i.get(e,!0,n)),void 0===o&&(o=Be(e,t,r)),"normal"===o&&t in We&&(o=We[t]),""===n||n?(a=parseFloat(o),!0===n||isFinite(a)?a||0:o):o}}),w.each(["height","width"],function(e,t){w.cssHooks[t]={get:function(e,n,r){if(n)return!He.test(w.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?Ke(e,t,r):oe(e,Ue,function(){return Ke(e,t,r)})},set:function(e,n,r){var o,a=$e(e),i="border-box"===w.css(e,"boxSizing",!1,a),s=r&&Ge(e,t,r,i,a);return i&&h.scrollboxSize()===a.position&&(s-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(a[t])-Ge(e,t,"border",!1,a)-.5)),s&&(o=te.exec(n))&&"px"!==(o[3]||"px")&&(e.style[t]=n,n=w.css(e,t)),Ye(0,n,s)}}}),w.cssHooks.marginLeft=Me(h.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Be(e,"marginLeft"))||e.getBoundingClientRect().left-oe(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),w.each({margin:"",padding:"",border:"Width"},function(e,t){w.cssHooks[e+t]={expand:function(n){for(var r=0,o={},a="string"==typeof n?n.split(" "):[n];r<4;r++)o[e+ne[r]+t]=a[r]||a[r-2]||a[0];return o}},"margin"!==e&&(w.cssHooks[e+t].set=Ye)}),w.fn.extend({css:function(e,t){return q(this,function(e,t,n){var r,o,a={},i=0;if(Array.isArray(t)){for(r=$e(e),o=t.length;i1)}}),w.Tween=Qe,Qe.prototype={constructor:Qe,init:function(e,t,n,r,o,a){this.elem=e,this.prop=n,this.easing=o||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=a||(w.cssNumber[n]?"":"px")},cur:function(){var e=Qe.propHooks[this.prop];return e&&e.get?e.get(this):Qe.propHooks._default.get(this)},run:function(e){var t,n=Qe.propHooks[this.prop];return this.options.duration?this.pos=t=w.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):Qe.propHooks._default.set(this),this}},Qe.prototype.init.prototype=Qe.prototype,Qe.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=w.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){w.fx.step[e.prop]?w.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[w.cssProps[e.prop]]&&!w.cssHooks[e.prop]?e.elem[e.prop]=e.now:w.style(e.elem,e.prop,e.now+e.unit)}}},Qe.propHooks.scrollTop=Qe.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},w.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},w.fx=Qe.prototype.init,w.fx.step={};var Ze,Je,et=/^(?:toggle|show|hide)$/,tt=/queueHooks$/;function nt(){Je&&(!1===r.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(nt):e.setTimeout(nt,w.fx.interval),w.fx.tick())}function rt(){return e.setTimeout(function(){Ze=void 0}),Ze=Date.now()}function ot(e,t){var n,r=0,o={height:e};for(t=t?1:0;r<4;r+=2-t)o["margin"+(n=ne[r])]=o["padding"+n]=e;return t&&(o.opacity=o.width=e),o}function at(e,t,n){for(var r,o=(it.tweeners[t]||[]).concat(it.tweeners["*"]),a=0,i=o.length;a1)},removeAttr:function(e){return this.each(function(){w.removeAttr(this,e)})}}),w.extend({attr:function(e,t,n){var r,o,a=e.nodeType;if(3!==a&&8!==a&&2!==a)return void 0===e.getAttribute?w.prop(e,t,n):(1===a&&w.isXMLDoc(e)||(o=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?st:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):o&&"set"in o&&void 0!==(r=o.set(e,n,t))?r:(e.setAttribute(t,n+""),n):o&&"get"in o&&null!==(r=o.get(e,t))?r:null==(r=w.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!h.radioValue&&"radio"===t&&I(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,o=t&&t.match(L);if(o&&1===e.nodeType)for(;n=o[r++];)e.removeAttribute(n)}}),st={set:function(e,t,n){return!1===t?w.removeAttr(e,n):e.setAttribute(n,n),n}},w.each(w.expr.match.bool.source.match(/\w+/g),function(e,t){var n=lt[t]||w.find.attr;lt[t]=function(e,t,r){var o,a,i=t.toLowerCase();return r||(a=lt[i],lt[i]=o,o=null!=n(e,t,r)?i:null,lt[i]=a),o}});var ct=/^(?:input|select|textarea|button)$/i,ut=/^(?:a|area)$/i;function dt(e){return(e.match(L)||[]).join(" ")}function ft(e){return e.getAttribute&&e.getAttribute("class")||""}function pt(e){return Array.isArray(e)?e:"string"==typeof e&&e.match(L)||[]}w.fn.extend({prop:function(e,t){return q(this,w.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[w.propFix[e]||e]})}}),w.extend({prop:function(e,t,n){var r,o,a=e.nodeType;if(3!==a&&8!==a&&2!==a)return 1===a&&w.isXMLDoc(e)||(t=w.propFix[t]||t,o=w.propHooks[t]),void 0!==n?o&&"set"in o&&void 0!==(r=o.set(e,n,t))?r:e[t]=n:o&&"get"in o&&null!==(r=o.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,"tabindex");return t?parseInt(t,10):ct.test(e.nodeName)||ut.test(e.nodeName)&&e.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),h.optSelected||(w.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),w.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){w.propFix[this.toLowerCase()]=this}),w.fn.extend({addClass:function(e){var t,n,r,o,a,i,s,l=0;if(m(e))return this.each(function(t){w(this).addClass(e.call(this,t,ft(this)))});if((t=pt(e)).length)for(;n=this[l++];)if(o=ft(n),r=1===n.nodeType&&" "+dt(o)+" "){for(i=0;a=t[i++];)r.indexOf(" "+a+" ")<0&&(r+=a+" ");o!==(s=dt(r))&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,o,a,i,s,l=0;if(m(e))return this.each(function(t){w(this).removeClass(e.call(this,t,ft(this)))});if(!arguments.length)return this.attr("class","");if((t=pt(e)).length)for(;n=this[l++];)if(o=ft(n),r=1===n.nodeType&&" "+dt(o)+" "){for(i=0;a=t[i++];)for(;r.indexOf(" "+a+" ")>-1;)r=r.replace(" "+a+" "," ");o!==(s=dt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(e,t){var n=typeof e,r="string"===n||Array.isArray(e);return"boolean"==typeof t&&r?t?this.addClass(e):this.removeClass(e):m(e)?this.each(function(n){w(this).toggleClass(e.call(this,n,ft(this),t),t)}):this.each(function(){var t,o,a,i;if(r)for(o=0,a=w(this),i=pt(e);t=i[o++];)a.hasClass(t)?a.removeClass(t):a.addClass(t);else void 0!==e&&"boolean"!==n||((t=ft(this))&&G.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":G.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;for(t=" "+e+" ";n=this[r++];)if(1===n.nodeType&&(" "+dt(ft(n))+" ").indexOf(t)>-1)return!0;return!1}});var ht=/\r/g;w.fn.extend({val:function(e){var t,n,r,o=this[0];return arguments.length?(r=m(e),this.each(function(n){var o;1===this.nodeType&&(null==(o=r?e.call(this,n,w(this).val()):e)?o="":"number"==typeof o?o+="":Array.isArray(o)&&(o=w.map(o,function(e){return null==e?"":e+""})),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,o,"value")||(this.value=o))})):o?(t=w.valHooks[o.type]||w.valHooks[o.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(o,"value"))?n:"string"==typeof(n=o.value)?n.replace(ht,""):null==n?"":n:void 0}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,"value");return null!=t?t:dt(w.text(e))}},select:{get:function(e){var t,n,r,o=e.options,a=e.selectedIndex,i="select-one"===e.type,s=i?null:[],l=i?a+1:o.length;for(r=a<0?l:i?a:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),a}}}}),w.each(["radio","checkbox"],function(){w.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=w.inArray(w(e).val(),t)>-1}},h.checkOn||(w.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),h.focusin="onfocusin"in e;var mt=/^(?:focusinfocus|focusoutblur)$/,gt=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(t,n,o,a){var i,s,l,c,u,f,p,h,v=[o||r],y=d.call(t,"type")?t.type:t,b=d.call(t,"namespace")?t.namespace.split("."):[];if(s=h=l=o=o||r,3!==o.nodeType&&8!==o.nodeType&&!mt.test(y+w.event.triggered)&&(y.indexOf(".")>-1&&(y=(b=y.split(".")).shift(),b.sort()),u=y.indexOf(":")<0&&"on"+y,(t=t[w.expando]?t:new w.Event(y,"object"==typeof t&&t)).isTrigger=a?2:3,t.namespace=b.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+b.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=o),n=null==n?[t]:w.makeArray(n,[t]),p=w.event.special[y]||{},a||!p.trigger||!1!==p.trigger.apply(o,n))){if(!a&&!p.noBubble&&!g(o)){for(c=p.delegateType||y,mt.test(c+y)||(s=s.parentNode);s;s=s.parentNode)v.push(s),l=s;l===(o.ownerDocument||r)&&v.push(l.defaultView||l.parentWindow||e)}for(i=0;(s=v[i++])&&!t.isPropagationStopped();)h=s,t.type=i>1?c:p.bindType||y,(f=(G.get(s,"events")||{})[t.type]&&G.get(s,"handle"))&&f.apply(s,n),(f=u&&s[u])&&f.apply&&X(s)&&(t.result=f.apply(s,n),!1===t.result&&t.preventDefault());return t.type=y,a||t.isDefaultPrevented()||p._default&&!1!==p._default.apply(v.pop(),n)||!X(o)||u&&m(o[y])&&!g(o)&&((l=o[u])&&(o[u]=null),w.event.triggered=y,t.isPropagationStopped()&&h.addEventListener(y,gt),o[y](),t.isPropagationStopped()&&h.removeEventListener(y,gt),w.event.triggered=void 0,l&&(o[u]=l)),t.result}},simulate:function(e,t,n){var r=w.extend(new w.Event,n,{type:e,isSimulated:!0});w.event.trigger(r,null,t)}}),w.fn.extend({trigger:function(e,t){return this.each(function(){w.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return w.event.trigger(e,t,n,!0)}}),h.focusin||w.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){w.event.simulate(t,e.target,w.event.fix(e))};w.event.special[t]={setup:function(){var r=this.ownerDocument||this,o=G.access(r,t);o||r.addEventListener(e,n,!0),G.access(r,t,(o||0)+1)},teardown:function(){var r=this.ownerDocument||this,o=G.access(r,t)-1;o?G.access(r,t,o):(r.removeEventListener(e,n,!0),G.remove(r,t))}}});var vt=e.location,yt=Date.now(),bt=/\?/;w.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,"text/xml")}catch(e){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||w.error("Invalid XML: "+t),n};var wt=/\[\]$/,xt=/\r?\n/g,Ct=/^(?:submit|button|image|reset|file)$/i,St=/^(?:input|select|textarea|keygen)/i;function Tt(e,t,n,r){var o;if(Array.isArray(t))w.each(t,function(t,o){n||wt.test(e)?r(e,o):Tt(e+"["+("object"==typeof o&&null!=o?t:"")+"]",o,n,r)});else if(n||"object"!==b(t))r(e,t);else for(o in t)Tt(e+"["+o+"]",t[o],n,r)}w.param=function(e,t){var n,r=[],o=function(e,t){var n=m(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(e)||e.jquery&&!w.isPlainObject(e))w.each(e,function(){o(this.name,this.value)});else for(n in e)Tt(n,e[n],t,o);return r.join("&")},w.fn.extend({serialize:function(){return w.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=w.prop(this,"elements");return e?w.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!w(this).is(":disabled")&&St.test(this.nodeName)&&!Ct.test(e)&&(this.checked||!ce.test(e))}).map(function(e,t){var n=w(this).val();return null==n?null:Array.isArray(n)?w.map(n,function(e){return{name:t.name,value:e.replace(xt,"\r\n")}}):{name:t.name,value:n.replace(xt,"\r\n")}}).get()}});var _t=/%20/g,Dt=/#.*$/,It=/([?&])_=[^&]*/,At=/^(.*?):[ \t]*([^\r\n]*)$/gm,kt=/^(?:GET|HEAD)$/,Ot=/^\/\//,Et={},Pt={},Ft="*/".concat("*"),Rt=r.createElement("a");function Lt(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,o=0,a=t.toLowerCase().match(L)||[];if(m(n))for(;r=a[o++];)"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function Nt(e,t,n,r){var o={},a=e===Pt;function i(s){var l;return o[s]=!0,w.each(e[s]||[],function(e,s){var c=s(t,n,r);return"string"!=typeof c||a||o[c]?a?!(l=c):void 0:(t.dataTypes.unshift(c),i(c),!1)}),l}return i(t.dataTypes[0])||!o["*"]&&i("*")}function $t(e,t){var n,r,o=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((o[n]?e:r||(r={}))[n]=t[n]);return r&&w.extend(!0,e,r),e}Rt.href=vt.href,w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:vt.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(vt.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Ft,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":w.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?$t($t(e,w.ajaxSettings),t):$t(w.ajaxSettings,e)},ajaxPrefilter:Lt(Et),ajaxTransport:Lt(Pt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var o,a,i,s,l,c,u,d,f,p,h=w.ajaxSetup({},n),m=h.context||h,g=h.context&&(m.nodeType||m.jquery)?w(m):w.event,v=w.Deferred(),y=w.Callbacks("once memory"),b=h.statusCode||{},x={},C={},S="canceled",T={readyState:0,getResponseHeader:function(e){var t;if(u){if(!s)for(s={};t=At.exec(i);)s[t[1].toLowerCase()]=t[2];t=s[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return u?i:null},setRequestHeader:function(e,t){return null==u&&(e=C[e.toLowerCase()]=C[e.toLowerCase()]||e,x[e]=t),this},overrideMimeType:function(e){return null==u&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(u)T.always(e[T.status]);else for(t in e)b[t]=[b[t],e[t]];return this},abort:function(e){var t=e||S;return o&&o.abort(t),_(0,t),this}};if(v.promise(T),h.url=((t||h.url||vt.href)+"").replace(Ot,vt.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(L)||[""],null==h.crossDomain){c=r.createElement("a");try{c.href=h.url,c.href=c.href,h.crossDomain=Rt.protocol+"//"+Rt.host!=c.protocol+"//"+c.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=w.param(h.data,h.traditional)),Nt(Et,h,n,T),u)return T;for(f in(d=w.event&&h.global)&&0==w.active++&&w.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!kt.test(h.type),a=h.url.replace(Dt,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(_t,"+")):(p=h.url.slice(a.length),h.data&&(h.processData||"string"==typeof h.data)&&(a+=(bt.test(a)?"&":"?")+h.data,delete h.data),!1===h.cache&&(a=a.replace(It,"$1"),p=(bt.test(a)?"&":"?")+"_="+yt+++p),h.url=a+p),h.ifModified&&(w.lastModified[a]&&T.setRequestHeader("If-Modified-Since",w.lastModified[a]),w.etag[a]&&T.setRequestHeader("If-None-Match",w.etag[a])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&T.setRequestHeader("Content-Type",h.contentType),T.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+Ft+"; q=0.01":""):h.accepts["*"]),h.headers)T.setRequestHeader(f,h.headers[f]);if(h.beforeSend&&(!1===h.beforeSend.call(m,T,h)||u))return T.abort();if(S="abort",y.add(h.complete),T.done(h.success),T.fail(h.error),o=Nt(Pt,h,n,T)){if(T.readyState=1,d&&g.trigger("ajaxSend",[T,h]),u)return T;h.async&&h.timeout>0&&(l=e.setTimeout(function(){T.abort("timeout")},h.timeout));try{u=!1,o.send(x,_)}catch(e){if(u)throw e;_(-1,e)}}else _(-1,"No Transport");function _(t,n,r,s){var c,f,p,x,C,S=n;u||(u=!0,l&&e.clearTimeout(l),o=void 0,i=s||"",T.readyState=t>0?4:0,c=t>=200&&t<300||304===t,r&&(x=function(e,t,n){for(var r,o,a,i,s=e.contents,l=e.dataTypes;"*"===l[0];)l.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(o in s)if(s[o]&&s[o].test(r)){l.unshift(o);break}if(l[0]in n)a=l[0];else{for(o in n){if(!l[0]||e.converters[o+" "+l[0]]){a=o;break}i||(i=o)}a=a||i}if(a)return a!==l[0]&&l.unshift(a),n[a]}(h,T,r)),x=function(e,t,n,r){var o,a,i,s,l,c={},u=e.dataTypes.slice();if(u[1])for(i in e.converters)c[i.toLowerCase()]=e.converters[i];for(a=u.shift();a;)if(e.responseFields[a]&&(n[e.responseFields[a]]=t),!l&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),l=a,a=u.shift())if("*"===a)a=l;else if("*"!==l&&l!==a){if(!(i=c[l+" "+a]||c["* "+a]))for(o in c)if((s=o.split(" "))[1]===a&&(i=c[l+" "+s[0]]||c["* "+s[0]])){!0===i?i=c[o]:!0!==c[o]&&(a=s[0],u.unshift(s[1]));break}if(!0!==i)if(i&&e.throws)t=i(t);else try{t=i(t)}catch(e){return{state:"parsererror",error:i?e:"No conversion from "+l+" to "+a}}}return{state:"success",data:t}}(h,x,T,c),c?(h.ifModified&&((C=T.getResponseHeader("Last-Modified"))&&(w.lastModified[a]=C),(C=T.getResponseHeader("etag"))&&(w.etag[a]=C)),204===t||"HEAD"===h.type?S="nocontent":304===t?S="notmodified":(S=x.state,f=x.data,c=!(p=x.error))):(p=S,!t&&S||(S="error",t<0&&(t=0))),T.status=t,T.statusText=(n||S)+"",c?v.resolveWith(m,[f,S,T]):v.rejectWith(m,[T,S,p]),T.statusCode(b),b=void 0,d&&g.trigger(c?"ajaxSuccess":"ajaxError",[T,h,c?f:p]),y.fireWith(m,[T,S]),d&&(g.trigger("ajaxComplete",[T,h]),--w.active||w.event.trigger("ajaxStop")))}return T},getJSON:function(e,t,n){return w.get(e,t,n,"json")},getScript:function(e,t){return w.get(e,void 0,t,"script")}}),w.each(["get","post"],function(e,t){w[t]=function(e,n,r,o){return m(n)&&(o=o||r,r=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:o,data:n,success:r},w.isPlainObject(e)&&e))}}),w._evalUrl=function(e){return w.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,throws:!0})},w.fn.extend({wrapAll:function(e){var t;return this[0]&&(m(e)&&(e=e.call(this[0])),t=w(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){for(var e=this;e.firstElementChild;)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return m(e)?this.each(function(t){w(this).wrapInner(e.call(this,t))}):this.each(function(){var t=w(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=m(e);return this.each(function(n){w(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){w(this).replaceWith(this.childNodes)}),this}}),w.expr.pseudos.hidden=function(e){return!w.expr.pseudos.visible(e)},w.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},w.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var jt={0:200,1223:204},Bt=w.ajaxSettings.xhr();h.cors=!!Bt&&"withCredentials"in Bt,h.ajax=Bt=!!Bt,w.ajaxTransport(function(t){var n,r;if(h.cors||Bt&&!t.crossDomain)return{send:function(o,a){var i,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(i in t.xhrFields)s[i]=t.xhrFields[i];for(i in t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||o["X-Requested-With"]||(o["X-Requested-With"]="XMLHttpRequest"),o)s.setRequestHeader(i,o[i]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?a(0,"error"):a(s.status,s.statusText):a(jt[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout(function(){n&&r()})},n=n("abort");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}}),w.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),w.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return w.globalEval(e),e}}}),w.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),w.ajaxTransport("script",function(e){var t,n;if(e.crossDomain)return{send:function(o,a){t=w("