diff --git a/.jshintrc b/.jshintrc index a4bf3293..757b37a9 100644 --- a/.jshintrc +++ b/.jshintrc @@ -14,7 +14,7 @@ "node": true, // Allow ES6. - "esversion": 6, + "esversion": 7, /* * ENFORCING OPTIONS diff --git a/README.md b/README.md index 8cc71042..bdf15843 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ Mapping tool for [*EVE ONLINE*](https://www.eveonline.com) - [wiki](https://github.com/exodus4d/pathfinder/wiki) - Developer [Slack](https://slack.com) chat: - https://pathfinder-eve-online.slack.com - - Please send me a mail for invite: pathfinder@exodus4d.de + - Join channel [pathfinder-eve-online.slack.com](https://join.slack.com/t/pathfinder-eve-online/shared_invite/enQtMzMyOTkyMjczMTA3LWI2NGE1OTY5ODBmNDZlMDY3MDIzYjk5ZTljM2JjZjIwNDRkNzMyMTEwMDUzOGQwM2E3ZjE1NGEwNThlMzYzY2Y) + - Can´t join? pathfinder@exodus4d.de **Feel free to check the code for bugs and security issues. Issues should be reported in the [Issue](https://github.com/exodus4d/pathfinder/issues) section.** @@ -49,7 +50,7 @@ Issues should be reported in the [Issue](https://github.com/exodus4d/pathfinder/ |-- app.js --> require.js config (!required for production!) |-- [0777] logs/ --> log files |-- ... - | -- node_modules/ --> node.js modules (not used for production) + |-- node_modules/ --> node.js modules (not used for production) |-- ... |-- [0755] public/ --> frontend source |-- css/ --> CSS dist/build folder (minified) diff --git a/app/cron.ini b/app/cron.ini index 08671e47..1eb66ce9 100644 --- a/app/cron.ini +++ b/app/cron.ini @@ -59,8 +59,13 @@ deleteStatisticsData = Cron\StatisticsUpdate->deleteStatisticsD ; truncate map history log files truncateMapHistoryLogFiles = Cron\MapHistory->truncateFiles, @halfHour -; updates small amount of static system data from CCP API +; sync "sovereignty" and "faction warfare" data from CCP´s ESI API +updateSovereigntyData = Cron\Universe->updateSovereigntyData, @halfPastHour + +; sync static system data from CCP´s ESI API +; -> Job is WIP! ;updateUniverseSystems = Cron\Universe->updateUniverseSystems, @instant -; setup universe DB with static data from ESI +; bootstrap job for "eve_universe" DB from CCP´s ESI API +; -> Only for development! This job is used to build the initial export/sql/eve_universe.sql ;setup = Cron\Universe->setup, @instant \ No newline at end of file diff --git a/app/lib/db/cortex.php b/app/lib/db/cortex.php index f81379cf..a8e5b5e0 100644 --- a/app/lib/db/cortex.php +++ b/app/lib/db/cortex.php @@ -213,7 +213,7 @@ class Cortex extends Cursor { } else $this->whitelist=$fields; $id=$this->dbsType=='sql'?$this->primary:'_id'; - if (!in_array($id,$this->whitelist)) + if (!in_array($id,$this->whitelist) && !($exclude && in_array($id,$fields))) $this->whitelist[]=$id; $this->applyWhitelist(); return $this->whitelist; @@ -595,7 +595,7 @@ class Cortex extends Cursor { * @param array|null $filter * @param array|null $options * @param int $ttl - * @return CortexCollection + * @return CortexCollection|false */ public function find($filter = NULL, array $options = NULL, $ttl = 0) { $sort=false; @@ -731,12 +731,12 @@ class Cortex extends Cursor { $addToFilter = array($id.' IN ?', $result); } // *-to-one - elseif ($this->dbsType == 'sql') { + elseif (!$deep && $this->dbsType == 'sql') { // use sub-query inclusion $has_filter=$this->mergeFilter([$has_filter, [$this->rel($key)->getTable().'.'.$fromConf[1].'='.$this->getTable().'.'.$id]]); $result = $this->_refSubQuery($key,$has_filter,$has_options); - $addToFilter = ['exists('.$result[0].')']+$result[1]; + $addToFilter = array_merge(['exists('.$result[0].')'],$result[1]); } elseif ($result = $this->_hasRefsIn($key,$has_filter,$has_options,$ttl)) $addToFilter = array($id.' IN ?', $result); @@ -781,11 +781,11 @@ class Cortex extends Cursor { $options['order'] = preg_replace('/\h+DESC(?=\s*(?:$|,))/i',' DESC NULLS LAST',$options['order']); // assemble full sql query for joined queries if ($hasJoin) { + $adhoc=[]; // when in count-mode and grouping is active, wrap the query later // otherwise add a an adhoc counter field here if (!($subquery_mode=($options && !empty($options['group']))) && $count) - $this->adhoc['_rows']=['expr'=>'COUNT(*)','value'=>NULL]; - $adhoc=[]; + $adhoc[]='(COUNT(*)) as _rows'; if (!$count) // add bind parameters for filters in adhoc fields if ($this->preBinds) { @@ -1224,7 +1224,7 @@ class Cortex extends Cursor { // m:m save cascade if (!empty($this->saveCsd)) { foreach($this->saveCsd as $key => $val) { - if($fields[$key]['relType'] == 'has-many') { + if ($fields[$key]['relType'] == 'has-many') { $relConf = $fields[$key]['has-many']; if ($relConf['hasRel'] == 'has-many') { $mmTable = $this->mmTable($relConf,$key); @@ -1236,12 +1236,12 @@ class Cortex extends Cursor { $filter[] = $id; } // delete all refs - if (is_null($val)) + if (empty($val)) $mm->erase($filter); // update refs elseif (is_array($val)) { $mm->erase($filter); - foreach($val as $v) { + foreach(array_unique($val) as $v) { if ($relConf['isSelf'] && $v==$id) continue; $mm->set($key,$v); @@ -1256,7 +1256,7 @@ class Cortex extends Cursor { $rel = $this->getRelInstance($relConf[0],$relConf,$key); // find existing relations $refs = $rel->find([$relConf[1].' = ?',$this->getRaw($relConf['relField'])]); - if (is_null($val)) { + if (empty($val)) { foreach ($refs?:[] as $model) { $model->set($relConf[1],NULL); $model->save(); @@ -1470,7 +1470,7 @@ class Cortex extends Cursor { // handle relations if (isset($fields[$key]['belongs-to-one'])) { // one-to-many, one-to-one - if (is_null($val)) + if (empty($val)) $val = NULL; elseif (is_object($val) && !($this->dbsType=='mongo' && ( @@ -1489,7 +1489,7 @@ class Cortex extends Cursor { $val = $this->db->legacy() ? new \MongoId($val) : new \MongoDB\BSON\ObjectId($val); } elseif (isset($fields[$key]['has-one'])){ $relConf = $fields[$key]['has-one']; - if (is_null($val)) { + if (empty($val)) { $val = $this->get($key); $val->set($relConf[1],NULL); } else { @@ -2554,7 +2554,7 @@ class CortexQueryParser extends \Prefab { function($match) use($db) { if (!isset($match[1])) return $match[0]; - if (preg_match('/\b(AND|OR|IN|LIKE|NOT)\b/i',$match[1])) + if (preg_match('/\b(AND|OR|IN|LIKE|NOT|HAVING|SELECT|FROM|WHERE)\b/i',$match[1])) return $match[1]; return $db->quotekey($match[1]); }, $cond); @@ -2576,7 +2576,7 @@ class CortexQueryParser extends \Prefab { function($match) use($table) { if (!isset($match[3])) return $match[1]; - if (preg_match('/\b(AND|OR|IN|LIKE|NOT)\b/i',$match[3])) + if (preg_match('/\b(AND|OR|IN|LIKE|NOT|HAVING|SELECT|FROM|WHERE)\b/i',$match[3])) return $match[0]; return $match[2].$table.'.'.$match[3]; }, $cond); diff --git a/app/main/controller/accesscontroller.php b/app/main/controller/accesscontroller.php index 26692c7e..ab2dca79 100644 --- a/app/main/controller/accesscontroller.php +++ b/app/main/controller/accesscontroller.php @@ -36,14 +36,15 @@ class AccessController extends Controller { } /** - * get current character and check if it is a valid character + * check login status and look or a valid character * @param \Base $f3 * @return string * @throws \Exception */ protected function isLoggedIn(\Base $f3) : string { $loginStatus = 'UNKNOWN'; - if($character = $this->getCharacter()){ + // disable ttl cache time here. Further getCharacter() calls should use a short ttl + if($character = $this->getCharacter(0)){ if($character->checkLoginTimer()){ if(( $authStatus = $character->isAuthorized()) === 'OK'){ $loginStatus = 'OK'; diff --git a/app/main/controller/admin.php b/app/main/controller/admin.php index 0ceccb00..7c9670df 100644 --- a/app/main/controller/admin.php +++ b/app/main/controller/admin.php @@ -89,7 +89,7 @@ class Admin extends Controller{ protected function getAdminCharacter(\Base $f3){ $adminCharacter = null; if( !$f3->exists(Sso::SESSION_KEY_SSO_ERROR) ){ - if( $character = $this->getCharacter() ){ + if( $character = $this->getCharacter(0) ){ if(in_array($character->roleId->name, ['SUPER', 'CORPORATION'], true)){ // current character is admin $adminCharacter = $character; @@ -288,7 +288,7 @@ class Admin extends Controller{ $characters = []; // check if kickCharacters belong to same Corp as admin character // -> remove admin char from valid characters... - if( !empty($characterIds = array_diff( [$characterId], [$character->_id])) ){ + if( !empty($characterIds = array_diff([$characterId], [$character->_id])) ){ if($character->roleId->name === 'SUPER'){ if($filterCharacters = CharacterModel::getAll($characterIds)){ $characters = $filterCharacters; @@ -337,7 +337,7 @@ class Admin extends Controller{ $maps = $filterMaps; } }else{ - $maps = $character->getCorporation()->getMaps([$mapId], ['addInactive' => true, 'ignoreMapCount' => true]); + $maps = $character->getCorporation()->getMaps($mapId, ['addInactive' => true, 'ignoreMapCount' => true]); } return $maps; @@ -404,7 +404,7 @@ class Admin extends Controller{ $corporations = $this->getAccessibleCorporations($character); foreach($corporations as $corporation){ - if($maps = $corporation->getMaps([], ['addInactive' => true, 'ignoreMapCount' => true])){ + if($maps = $corporation->getMaps(null, ['addInactive' => true, 'ignoreMapCount' => true])){ $data->corpMaps[$corporation->name] = $maps; } } diff --git a/app/main/controller/api/map.php b/app/main/controller/api/map.php index 53f6898e..1fa570a2 100644 --- a/app/main/controller/api/map.php +++ b/app/main/controller/api/map.php @@ -52,13 +52,10 @@ class Map extends Controller\AccessController { * @throws Exception */ public function initData(\Base $f3){ - // expire time in seconds - $expireTimeCache = 60 * 60; - - if(!$f3->exists(self::CACHE_KEY_INIT, $return)){ - // response should not be cached if invalid -> e.g. missing static data - $validInitData = true; + $validInitData = true; + $ttl = 60 * 60; + if(!$exists = $f3->exists(self::CACHE_KEY_INIT, $return)){ $return = (object) []; $return->error = []; @@ -179,7 +176,10 @@ class Map extends Controller\AccessController { // get third party APIs ----------------------------------------------------------------------------------- $return->url = [ 'ccpImageServer' => Config::getPathfinderData('api.ccp_image_server'), - 'zKillboard' => Config::getPathfinderData('api.z_killboard') + 'zKillboard' => Config::getPathfinderData('api.z_killboard'), + 'eveeye' => Config::getPathfinderData('api.eveeye'), + 'dotlan' => Config::getPathfinderData('api.dotlan'), + 'anoik' => Config::getPathfinderData('api.anoik') ]; // Character default config ------------------------------------------------------------------------------- @@ -199,29 +199,33 @@ class Map extends Controller\AccessController { // structure status --------------------------------------------------------------------------------------- $structureStatus = Pathfinder\StructureStatusModel::getAll(); - $structureData = []; + $structureStatusData = []; foreach($structureStatus as $status){ - $structureData[$status->_id] = $status->getData(); + $structureStatusData[$status->_id] = $status->getData(); } - $return->structureStatus = $structureData; + $return->structureStatus = $structureStatusData; - $validInitData = $validInitData ? !empty($structureData) : $validInitData; + $validInitData = $validInitData ? !empty($structureStatusData) : $validInitData; // get available wormhole types --------------------------------------------------------------------------- /** - * @var $wormhole Universe\WormholeModel + * @var $groupUniverseModel Universe\GroupModel */ - $wormhole = Universe\AbstractUniverseModel::getNew('WormholeModel'); + $groupUniverseModel = Universe\AbstractUniverseModel::getNew('GroupModel'); + $groupUniverseModel->getById(Config::ESI_GROUP_WORMHOLE_ID); $wormholesData = []; - if($rows = $wormhole->find(null, ['order' => 'name asc'])){ - foreach($rows as $rowData){ - $wormholesData[$rowData->name] = $rowData->getData(); + /** + * @var $typeModel Universe\TypeModel + */ + foreach($types = $groupUniverseModel->getTypes(false) as $typeModel){ + if( + ($wormholeData = $typeModel->getWormholeData()) && + mb_strlen((string)$wormholeData->name) === 4 + ){ + $wormholesData[$wormholeData->name] = $wormholeData; } - - $wormhole->reset(); - $wormhole->name = 'K162'; - $wormholesData[$wormhole->name] = $wormhole->getData(); } + ksort($wormholesData); $return->wormholes = $wormholesData; $validInitData = $validInitData ? !empty($wormholesData) : $validInitData; @@ -231,20 +235,20 @@ class Map extends Controller\AccessController { * @var $categoryUniverseModel Universe\CategoryModel */ $categoryUniverseModel = Universe\AbstractUniverseModel::getNew('CategoryModel'); - $categoryUniverseModel->getById(6); - $shipData = $categoryUniverseModel->getData(['mass']); - $categoryUniverseModel->getById(65); - $structureData = $categoryUniverseModel->getData(); - $return->universeCategories = [ - 6 => $shipData, - 65 => $structureData + Config::ESI_CATEGORY_SHIP_ID => + ($categoryUniverseModel->getById(Config::ESI_CATEGORY_SHIP_ID) && $categoryUniverseModel->valid()) ? $categoryUniverseModel->getData(['mass']) : null, + Config::ESI_CATEGORY_STRUCTURE_ID => + ($categoryUniverseModel->getById(Config::ESI_CATEGORY_STRUCTURE_ID) && $categoryUniverseModel->valid()) ? $categoryUniverseModel->getData() : null, ]; - $validInitData = $validInitData ? !empty($return->universeCategories[65]) : $validInitData; + $validInitData = $validInitData ? !count(array_filter($return->universeCategories, function($v){ + return empty(array_filter((array)$v->groups)); + })) : $validInitData; + // response should not be cached if invalid -> e.g. missing static data if($validInitData){ - $f3->set(self::CACHE_KEY_INIT, $return, $expireTimeCache ); + $f3->set(self::CACHE_KEY_INIT, $return, $ttl); } } @@ -257,6 +261,9 @@ class Map extends Controller\AccessController { $ssoError->message = $message; $return->error[] = $ssoError; $f3->clear(Controller\Ccp\Sso::SESSION_KEY_SSO_ERROR); + }elseif($validInitData){ + // no errors and valid data -> send Cache header + $f3->expire(Config::ttlLeft($exists, $ttl)); } echo json_encode($return); @@ -871,6 +878,7 @@ class Map extends Controller\AccessController { $getMapUserData = (bool)$postData['getMapUserData']; $mapTracking = (bool)$postData['mapTracking']; $systemData = (array)$postData['systemData']; + $newSystemPositions = (array)$postData['newSystemPositions']; $activeCharacter = $this->getCharacter(); $return = (object)[]; @@ -886,7 +894,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->updateMapByCharacter($map, $activeCharacter); + $map = $this->updateMapByCharacter($map, $activeCharacter, $newSystemPositions); } // mapUserData ---------------------------------------------------------------------------------------- @@ -906,14 +914,13 @@ class Map extends Controller\AccessController { // systemData ----------------------------------------------------------------------------------------- if( $mapId === (int)$systemData['mapId'] && - !is_null($system = $map->getSystemById((int)$systemData['systemData']['id'])) + !is_null($system = $map->getSystemById((int)$systemData['id'])) ){ // data for currently selected system $return->system = $system->getData(); $return->system->signatures = $system->getSignaturesData(); $return->system->sigHistory = $system->getSignaturesHistory(); $return->system->structures = $system->getStructuresData(); - } } } @@ -932,10 +939,11 @@ class Map extends Controller\AccessController { * update map connections/systems based on $character´s location logs * @param Pathfinder\MapModel $map * @param Pathfinder\CharacterModel $character + * @param array $newSystemPositions * @return Pathfinder\MapModel * @throws Exception */ - protected function updateMapByCharacter(Pathfinder\MapModel $map, Pathfinder\CharacterModel $character) : Pathfinder\MapModel { + protected function updateMapByCharacter(Pathfinder\MapModel $map, Pathfinder\CharacterModel $character, array $newSystemPositions = []) : Pathfinder\MapModel { // map changed. update cache (system/connection) changed $mapDataChanged = false; @@ -952,6 +960,9 @@ class Map extends Controller\AccessController { $sourceSystemId = (int)$sourceLog->systemId; if($sourceSystemId){ + $defaultPositions = (array)$newSystemPositions['defaults']; + $currentPosition = (array)$newSystemPositions['location']; + $sourceSystem = null; $targetSystem = null; @@ -963,8 +974,8 @@ class Map extends Controller\AccessController { // system coordinates for system tha might be added next $systemOffsetX = 130; $systemOffsetY = 0; - $systemPosX = 0; - $systemPosY = 30; + $systemPosX = ((int)$defaultPositions[0]['x']) ? : 0; + $systemPosY = ((int)$defaultPositions[0]['y']) ? : 30; // check if previous (solo) system is already on the map ---------------------------------------------- $sourceSystem = $map->getSystemByCCPId($sourceSystemId, [AbstractModel::getFilter('active', true)]); @@ -972,12 +983,10 @@ class Map extends Controller\AccessController { // if systems don´t already exists on map -> get "blank" system // -> required for system type check (e.g. wormhole, k-space) if($sourceSystem){ + // system exists $sourceExists = true; - - // system exists -> add target to the "right" - $systemPosX = $sourceSystem->posX + $systemOffsetX; - $systemPosY = $sourceSystem->posY + $systemOffsetY; }else{ + // system not exists -> get"blank" system $sourceSystem = $map->getNewSystem($sourceSystemId); } @@ -992,6 +1001,11 @@ class Map extends Controller\AccessController { if($targetSystem){ $targetExists = true; + + if($targetSystemId === (int)$currentPosition['systemId']){ + $systemPosX = (int)$currentPosition['position']['x']; + $systemPosY = (int)$currentPosition['position']['y']; + } }else{ $targetSystem = $map->getNewSystem($targetSystemId); } @@ -1062,6 +1076,24 @@ class Map extends Controller\AccessController { break; } + // check for "abyss" systems ===================================================================== + if(!$map->trackAbyssalJumps){ + if( + $sourceSystem->isAbyss() || + $targetSystem->isAbyss() + ){ + $addConnection = false; + + if($sourceSystem->isAbyss()){ + $addSourceSystem = false; + } + + if($targetSystem->isAbyss()){ + $addTargetSystem = false; + } + } + } + // save source system ============================================================================= if( $addSourceSystem && @@ -1074,9 +1106,15 @@ class Map extends Controller\AccessController { $map = $sourceSystem->mapId; $sourceExists = true; $mapDataChanged = true; - // increase system position (prevent overlapping) - $systemPosX = $sourceSystem->posX + $systemOffsetX; - $systemPosY = $sourceSystem->posY + $systemOffsetY; + + if(!empty($defaultPositions[1])){ + $systemPosX = (int)$defaultPositions[1]['x']; + $systemPosY = (int)$defaultPositions[1]['y']; + }else{ + // increase system position (prevent overlapping) + $systemPosX = $sourceSystem->posX + $systemOffsetX; + $systemPosY = $sourceSystem->posY + $systemOffsetY; + } } } diff --git a/app/main/controller/api/rest/signature.php b/app/main/controller/api/rest/signature.php index 08c0bce2..92a0db71 100644 --- a/app/main/controller/api/rest/signature.php +++ b/app/main/controller/api/rest/signature.php @@ -56,7 +56,7 @@ class Signature extends AbstractRestController { $data['groupId'] == 5 || $data['typeId'] == 0 ){ - unset( $data['typeId'] ); + unset($data['typeId']); } // "sig reader" should not overwrite signature group information @@ -65,6 +65,7 @@ class Signature extends AbstractRestController { $signature->groupId > 0 ){ unset($data['groupId']); + unset($data['typeId']); } } @@ -78,13 +79,20 @@ class Signature extends AbstractRestController { // delete "old" signatures ---------------------------------------------------------------------------- if((bool)$requestData['deleteOld']){ + // if linked ConnectionModels should be deleted as well + $deleteConnectionId = (bool)$requestData['deleteConnection']; + $updatedSignatureIds = array_column($signaturesData, 'id'); $signatures = $system->getSignatures(); foreach($signatures as $signature){ if(!in_array($signature->_id, $updatedSignatureIds)){ + // set if potential linked ConnectionModel should be deleted as well + $signature->virtual('connectionIdDeleteCascade', $deleteConnectionId); if($signature->delete()){ $updateSignaturesHistory = true; } + // clear temp virtual field as well + $signature->clearVirtual('connectionIdDeleteCascade'); } } } @@ -190,6 +198,9 @@ class Signature extends AbstractRestController { $system->getById($systemId); if($system->hasAccess($activeCharacter)){ + // if linked ConnectionModels should be deleted as well + $deleteConnectionId = (bool)$requestData['deleteConnection']; + // if there is any changed/deleted/updated signature // -> we need to update signature history data for the system $updateSignaturesHistory = false; @@ -202,11 +213,15 @@ class Signature extends AbstractRestController { $signature->getById($signatureId); // make sure signature belongs to main system (user has access) if($signature->get('systemId', true) == $systemId){ + // set if potential linked ConnectionModel should be deleted as well + $signature->virtual('connectionIdDeleteCascade', $deleteConnectionId); if($signature->delete()){ $deletedSignatureIds[] = $signatureId; $updateSignaturesHistory = true; } $signature->reset(); + // clear temp virtual field as well + $signature->clearVirtual('connectionIdDeleteCascade'); } } diff --git a/app/main/controller/api/rest/system.php b/app/main/controller/api/rest/system.php index 48cc16e3..4a5983a8 100644 --- a/app/main/controller/api/rest/system.php +++ b/app/main/controller/api/rest/system.php @@ -38,6 +38,7 @@ class System extends AbstractRestController { $systemData->signatures = $system->getSignaturesData(); $systemData->sigHistory = $system->getSignaturesHistory(); $systemData->structures = $system->getStructuresData(); + $systemData->stations = $system->getStationsData(); } } diff --git a/app/main/controller/api/route.php b/app/main/controller/api/route.php index 2ac25c50..a19fe354 100644 --- a/app/main/controller/api/route.php +++ b/app/main/controller/api/route.php @@ -91,14 +91,16 @@ class Route extends Controller\AccessController { * -> this data is equal for EACH route search (does not depend on map data) */ private function setStaticJumpData(){ - $query = "SELECT * FROM system_neighbour"; - $rows = $this->getDB()->exec($query, null, $this->staticJumpDataCacheTime); + if($universeDB = $this->getDB('UNIVERSE')){ + $query = "SELECT * FROM system_neighbour"; + $rows = $universeDB->exec($query, null, $this->staticJumpDataCacheTime); - if(count($rows) > 0){ - array_walk($rows, function(&$row){ - $row['jumpNodes'] = array_map('intval', explode(':', $row['jumpNodes'])); - }); - $this->updateJumpData($rows); + if(count($rows) > 0){ + array_walk($rows, function(&$row){ + $row['jumpNodes'] = array_map('intval', explode(':', $row['jumpNodes'])); + }); + $this->updateJumpData($rows); + } } } diff --git a/app/main/controller/api/setup.php b/app/main/controller/api/setup.php index a14d1109..000c5317 100644 --- a/app/main/controller/api/setup.php +++ b/app/main/controller/api/setup.php @@ -9,6 +9,7 @@ namespace Controller\Api; use Controller; +use lib\Config; use Model; class Setup extends Controller\Controller { @@ -23,6 +24,7 @@ class Setup extends Controller\Controller { public function buildIndex(\Base $f3){ $postData = (array)$f3->get('POST'); $type = (string)$postData['type']; + $countAll = (int)$postData['countAll']; $count = (int)$postData['count']; $offset = (int)$postData['offset']; @@ -31,7 +33,7 @@ class Setup extends Controller\Controller { $return->type = $type; $return->count = $count; $return->offset = $offset; - $return->countAll = 0; + $return->countAll = $countAll; $return->countBuild = 0; $return->countBuildAll = 0; $return->progress = 0; @@ -42,7 +44,7 @@ class Setup extends Controller\Controller { * @param int $value * @return int */ - $sum = function(int $carry, int $value){ + $sum = function(int $carry, int $value) : int { $carry += $value; return $carry; }; @@ -62,49 +64,72 @@ class Setup extends Controller\Controller { case 'Systems': $length = 100; $buildInfo = $controller->buildSystemsIndex($offset, $length); + $return->offset = $buildInfo['offset']; $return->countAll = $buildInfo['countAll']; $return->countBuild = $buildInfo['countBuild']; - $return->countBuildAll = $offset; - $return->progress = $percent($return->countAll, $offset); + $return->countBuildAll = $return->offset; + break; + case 'Wormholes': + $groupId = Config::ESI_GROUP_WORMHOLE_ID; + $length = 10; + $buildInfo = $controller->setupGroup($groupId, $offset, $length, true); + + $return->offset = $buildInfo['offset']; + $return->countAll = $buildInfo['countAll']; + $return->countBuild = $buildInfo['count']; + $return->countBuildAll = $return->offset; break; case 'Structures': - $categoryId = 65; - $length = 2; - $offset = $count * $length; + $categoryId = Config::ESI_CATEGORY_STRUCTURE_ID; + $length = 1; $buildInfo = $controller->setupCategory($categoryId, $offset, $length); - $categoryUniverseModel = Model\Universe\AbstractUniverseModel::getNew('CategoryModel'); $categoryUniverseModel->getById($categoryId, 0); - $return->countAll = (int)$f3->get('REQUIREMENTS.DATA.STRUCTURES'); - $return->countBuild = array_reduce($buildInfo, $sum, 0); - $return->countBuildAll = $categoryUniverseModel->getTypesCount(false); - $return->progress = $percent($return->countAll, $return->countBuildAll); + + $return->offset = $buildInfo['offset']; + $return->countBuild = $buildInfo['count']; + $return->countBuildAll = $return->offset; + $return->subCount = [ + 'countBuildAll' => $categoryUniverseModel->getTypesCount(false) + ]; break; case 'Ships': - $categoryId = 6; + $categoryId = Config::ESI_CATEGORY_SHIP_ID; $length = 2; - $offset = $count * $length; $buildInfo = $controller->setupCategory($categoryId, $offset, $length); - $categoryUniverseModel = Model\Universe\AbstractUniverseModel::getNew('CategoryModel'); $categoryUniverseModel->getById($categoryId, 0); - $return->countAll = (int)$f3->get('REQUIREMENTS.DATA.SHIPS'); - $return->countBuild = array_reduce($buildInfo, $sum, 0); - $return->countBuildAll = $categoryUniverseModel->getTypesCount(false); - $return->progress = $percent($return->countAll, $return->countBuildAll); + + $return->offset = $buildInfo['offset']; + $return->countBuild = $buildInfo['count']; + $return->countBuildAll = $return->offset; + $return->subCount = [ + 'countBuildAll' => $categoryUniverseModel->getTypesCount(false) + ]; + break; + case 'SystemStatic': + $length = 300; + $buildInfo = $this->setupSystemStaticTable($offset, $length); + + $return->offset = $buildInfo['offset']; + $return->countAll = $buildInfo['countAll']; + $return->countBuild = $buildInfo['count']; + $return->countBuildAll = $return->offset; break; case 'SystemNeighbour': - // Becomes deprecated with new Universe DB!!! - $this->setupSystemJumpTable(); + $length = 1500; + $buildInfo = $this->setupSystemJumpTable($offset, $length); - $return->countAll = (int)$f3->get('REQUIREMENTS.DATA.NEIGHBOURS'); - $return->countBuild = $f3->DB->getDB('PF')->getRowCount('system_neighbour'); - $return->countBuildAll = $return->countBuild; - $return->progress = $percent($return->countAll, $return->countBuildAll); + $return->offset = $buildInfo['offset']; + $return->countAll = $buildInfo['countAll']; + $return->countBuild = $buildInfo['count']; + $return->countBuildAll = $return->offset; break; } + $return->progress = $percent($return->countAll, $return->countBuildAll); + if($return->countBuildAll < $return->countAll){ $return->count++; } @@ -135,21 +160,87 @@ class Setup extends Controller\Controller { case 'Systems': $controller->clearSystemsIndex(); $systemUniverseModel = Model\Universe\AbstractUniverseModel::getNew('SystemModel'); - $return->countAll = $f3->DB->getDB('UNIVERSE')->getRowCount($systemUniverseModel->getTable()); + $return->countAll = $systemUniverseModel->getRowCount(); + break; + case 'SystemNeighbour': + $systemNeighbourModel = Model\Universe\AbstractUniverseModel::getNew('SystemNeighbourModel'); + $systemNeighbourModel->truncate(); + $return->countAll = (int)$f3->get('REQUIREMENTS.DATA.NEIGHBOURS'); break; } echo json_encode($return); } + /** + * import static 'system_static` table data from *.csv + * @param int $offset + * @param int $length + * @return array + * @throws \Exception + */ + protected function setupSystemStaticTable(int $offset = 0, int $length = 0) : array { + $info = ['countAll' => 0, 'countChunk' => 0, 'count' => 0, 'offset' => $offset]; + + /** + * @var $systemStaticModel Model\Universe\SystemStaticModel + */ + $systemStaticModel = Model\Universe\AbstractUniverseModel::getNew('SystemStaticModel'); + if(!empty($csvData = $systemStaticModel::getCSVData($systemStaticModel->getTable()))){ + $info['countAll'] = count($csvData); + if($length){ + $csvData = array_slice($csvData, $offset, $length); + } + $info['countChunk'] = count($csvData); + $cols = ['typeId' => [], 'systemId' => []]; + foreach($csvData as $data){ + $validColCount = 0; + $systemStaticModel->getById((int)$data['id'], 0); + $systemStaticModel->id = (int)$data['id']; + foreach($cols as $col => &$invalidIds){ + if($systemStaticModel->exists($col)){ + $colVal = (int)$data[$col]; + if(!in_array($colVal, $invalidIds)){ + $relModel = $systemStaticModel->rel($col); + $relModel->getById($colVal, 0); + if($relModel->valid()){ + $systemStaticModel->$col = $relModel; + $validColCount++; + }else{ + $invalidIds[] = $colVal; + break; + } + }else{ + break; + } + } + } + + if($validColCount == count($cols)){ + $systemStaticModel->save(); + } + $systemStaticModel->reset(); + + $info['count']++; + $info['offset']++; + } + } + + return $info; + } + /** * This function is just for setting up the cache table 'system_neighbour' which is used * for system jump calculation. Call this function manually when CCP adds Systems/Stargates + * @param int $offset + * @param int $length + * @return array */ - protected function setupSystemJumpTable(){ - $universeDB = $this->getDB('UNIVERSE'); + protected function setupSystemJumpTable(int $offset = 0, int $length = 0) : array { + $info = ['countAll' => 0, 'countChunk' => 0, 'count' => 0, 'offset' => $offset]; + $universeDB = $this->getDB('UNIVERSE'); - $query = "SELECT + $query = "SELECT SQL_CALC_FOUND_ROWS `system`.`id` `systemId`, `system`.`name` `systemName`, `system`.`constellationId` `constellationId`, @@ -178,58 +269,73 @@ class Setup extends Controller\Controller { `system`.`security` = :ls OR `system`.`security` = :hs ) + HAVING + `jumpNodes` IS NOT NULL "; - $rows = $universeDB->exec($query, [ + $args = [ ':regionIdJove1' => 10000017, ':regionIdJove2' => 10000019, ':regionIdJove3' => 10000004, ':ns' => '0.0', ':ls' => 'L', ':hs' => 'H' - ]); + ]; - if(count($rows)){ - $pfDB = $this->getDB('PF'); + if($length){ + $query .= ' LIMIT :limit'; + $args[':limit'] = $length; - // clear cache table - $pfDB->exec("TRUNCATE system_neighbour"); + if($offset){ + $query .= ' OFFSET :offset'; + $args[':offset'] = $offset; + } + } + $rows = $universeDB->exec($query, $args); + + if(!empty($countRes = $universeDB->exec("SELECT FOUND_ROWS() `count`")) && isset($countRes[0]['count'])){ + $info['countAll'] = (int)$countRes[0]['count']; + } + + if($info['countChunk'] = count($rows)){ + $placeholderStr = function(string $str) : string { + return ':' . $str; + }; + + $updateRule = function(string $str) : string { + return $str . " = VALUES(" . $str . ")"; + }; + + $universeDB->begin(); foreach($rows as $row){ + $info['count']++; + $info['offset']++; + if(!$row['jumpNodes']){ // should never happen! continue; } - $pfDB->exec(" - INSERT INTO - system_neighbour( - regionId, - constellationId, - systemName, - systemId, - jumpNodes, - trueSec - ) - VALUES( - :regionId, - :constellationId, - :systemName, - :systemId, - :jumpNodes, - :trueSec - )", - [ - ':regionId' => $row['regionId'], - ':constellationId' => $row['constellationId'], - ':systemName' => $row['systemName'], - ':systemId' => $row['systemId'], - ':jumpNodes' => $row['jumpNodes'], - ':trueSec' => $row['trueSec'] - ] - ); + $columns = array_keys($row); + $columnsQuoted = array_map($universeDB->quotekey, $columns); + $placeholder = array_map($placeholderStr, $columns); + $args = array_combine($placeholder, $row); + + $updateSql = array_map($updateRule, $columns); + + $sql = "INSERT INTO + system_neighbour(" . implode(', ', $columnsQuoted) . ") + VALUES(" . implode(', ', $placeholder) . ") + ON DUPLICATE KEY UPDATE + " . implode(', ', $updateSql); + + $universeDB->exec($sql, $args); } + $universeDB->commit(); } + + return $info; } } \ No newline at end of file diff --git a/app/main/controller/api/system.php b/app/main/controller/api/system.php index 1b80de87..31c65dba 100644 --- a/app/main/controller/api/system.php +++ b/app/main/controller/api/system.php @@ -111,7 +111,7 @@ class System extends Controller\AccessController { } /** - * set destination for specific systemIds + * set destination for system, station or structure * @param \Base $f3 * @throws \Exception */ @@ -120,25 +120,25 @@ class System extends Controller\AccessController { $return = (object) []; $return->error = []; - $return->systemData = []; + $return->destData = []; - if( !empty($postData['systemData'] )){ + if(!empty($destData = (array)$postData['destData'])){ $activeCharacter = $this->getCharacter(); $return->clearOtherWaypoints = (bool)$postData['clearOtherWaypoints']; $return->first = (bool)$postData['first']; - if( $accessToken = $activeCharacter->getAccessToken() ){ + if($accessToken = $activeCharacter->getAccessToken()){ $options = [ 'clearOtherWaypoints' => $return->clearOtherWaypoints, 'addToBeginning' => $return->first, ]; - foreach($postData['systemData'] as $systemData){ - $response = $f3->ccpClient()->setWaypoint($systemData['systemId'], $accessToken, $options); + foreach($destData as $data){ + $response = $f3->ccpClient()->setWaypoint((int)$data['id'], $accessToken, $options); if(empty($response)){ - $return->systemData[] = $systemData; + $return->destData[] = $data; }else{ $error = (object) []; $error->type = 'error'; diff --git a/app/main/controller/api/universe.php b/app/main/controller/api/universe.php index 559d0347..95156955 100644 --- a/app/main/controller/api/universe.php +++ b/app/main/controller/api/universe.php @@ -112,7 +112,7 @@ class Universe extends Controller\AccessController { $constellation = Model\Universe\AbstractUniverseModel::getNew('ConstellationModel'); $constellation->getById($constellationId); - if( !$constellation->dry() && $constellation->systems){ + if($constellation->valid() && $constellation->systems){ /** * @var Model\Universe\SystemModel $system */ diff --git a/app/main/controller/api/user.php b/app/main/controller/api/user.php index 03ae0c3a..9c2d387d 100644 --- a/app/main/controller/api/user.php +++ b/app/main/controller/api/user.php @@ -128,7 +128,7 @@ class User extends Controller\Controller{ // character is valid and allowed to login $return->character = reset($characters)->getData(); // get Session status for character - if($activeCharacter = $this->getCharacter()){ + if($activeCharacter = $this->getCharacter(0)){ if($activeUser = $activeCharacter->getUser()){ if($sessionCharacterData = $activeUser->findSessionCharacterData($return->character->id)){ $return->character->hasActiveSession = true; diff --git a/app/main/controller/ccp/sso.php b/app/main/controller/ccp/sso.php index 27e9cf9f..914ea340 100644 --- a/app/main/controller/ccp/sso.php +++ b/app/main/controller/ccp/sso.php @@ -81,7 +81,7 @@ class Sso extends Api\User{ // check if character is valid and exists if( - !$character->dry() && + $character->valid() && $character->hasUserCharacter() && ($activeCharacter->getUser()->_id === $character->getUser()->_id) ){ @@ -217,9 +217,9 @@ class Sso extends Api\User{ $characterModel = $characterModel->updateLog(); // connect character with current user - if( is_null($user = $this->getUser()) ){ + if(is_null($user = $this->getUser())){ // connect character with existing user (no changes) - if( is_null( $user = $characterModel->getUser()) ){ + if(is_null($user = $characterModel->getUser())){ // no user found (new character) -> create new user and connect to character /** * @var $user Pathfinder\UserModel @@ -234,7 +234,7 @@ class Sso extends Api\User{ * @var $userCharactersModel Pathfinder\UserCharacterModel */ if( is_null($userCharactersModel = $characterModel->userCharacter) ){ - $userCharactersModel = Pathfinder\AbstractPathfinderModel::getNew('UserCharacterModel'); + $userCharactersModel = $characterModel->rel('userCharacter'); $userCharactersModel->characterId = $characterModel; } diff --git a/app/main/controller/ccp/universe.php b/app/main/controller/ccp/universe.php index 94f1a142..67f1fc68 100644 --- a/app/main/controller/ccp/universe.php +++ b/app/main/controller/ccp/universe.php @@ -53,6 +53,25 @@ class Universe extends Controller { } }*/ + /** + * setup categories + all dependencies (e.g. groups, types) + * id 2 -> Celestial (>100 groups -> >1000 types) + * id 6 -> Ship (46 groups -> 4xx types) + * id 65 -> Structure (10 groups -> 33 types) + * @param array $categoriesWhitelist + * @return array + * @throws \Exception + */ + protected function setupCategories(array $categoriesWhitelist = []) : array { + $info = []; + $categoryIds = Model\Universe\CategoryModel::getUniverseCategories(); + $categoryIds = array_intersect($categoriesWhitelist, $categoryIds); + foreach($categoryIds as $categoryId){ + $info[$categoryId] = $this->setupCategory($categoryId); + } + return $info; + } + /** * setup category + all dependencies (e.g. groups, types) * -> $length = 0 -> setup all groups @@ -63,41 +82,18 @@ class Universe extends Controller { * @throws \Exception */ public function setupCategory(int $categoryId, int $offset = 0, int $length = 0) : array { - $return = []; + $info = ['countAll' => 0, 'countChunk' => 0, 'count' => 0, 'offset' => $offset, 'groupTypes' => []]; + if($categoryId){ /** * @var $category Model\Universe\CategoryModel */ $category = Model\Universe\AbstractUniverseModel::getNew('CategoryModel'); $category->loadById($categoryId); - $groupIds = $category->loadGroupsData($offset, $length); - foreach((array)$category->groups as $group){ - // only load types for changed groups (not all) - if(in_array($group->_id, $groupIds)){ - $return[$group->_id] = $group->loadTypesData(); - } - } + $info = $category->loadGroupsData($offset, $length); } - return $return; - } - /** - * setup categories + all dependencies (e.g. groups, types) - * id 2 -> Celestial (>100 groups -> >1000 types) - * id 6 -> Ship (45 groups -> 490 types) - * id 65 -> Structure (10 groups -> 33 types) - * @param array $categoriesWhitelist - * @return array - * @throws \Exception - */ - protected function setupCategories(array $categoriesWhitelist = []) : array { - $return = []; - $categoryIds = $this->getF3()->ccpClient()->getUniverseCategories(); - $categoryIds = array_intersect($categoriesWhitelist, $categoryIds); - foreach($categoryIds as $categoryId){ - $return[$categoryId] = $this->setupCategory($categoryId); - } - return $return; + return $info; } /** @@ -111,19 +107,38 @@ class Universe extends Controller { * @throws \Exception */ protected function setupGroups(array $groupsWhitelist = []) : array { - $return = []; - $groupIds = $this->getF3()->ccpClient()->getUniverseGroups(); + $info = []; + $groupIds = Model\Universe\GroupModel::getUniverseGroups(); $groupIds = array_intersect($groupsWhitelist, $groupIds); - /** - * @var $group Model\Universe\GroupModel - */ - $group = Model\Universe\AbstractUniverseModel::getNew('GroupModel'); foreach($groupIds as $groupId){ - $group->loadById($groupId); - $return[$group->_id] = $group->loadTypesData(); - $group->reset(); + $info[$groupId] = $this->setupGroup($groupId); } - return $return; + return $info; + } + + /** + * setup group + all dependencies (e.g. types) + * @param int $groupId + * @param int $offset + * @param int $length + * @param bool $storeDogmaAttributes + * @return array + * @throws \Exception + */ + public function setupGroup(int $groupId, int $offset = 0, int $length = 0, bool $storeDogmaAttributes = false) : array { + $info = ['countAll' => 0, 'countChunk' => 0, 'count' => 0, 'offset' => $offset]; + + if($groupId){ + /** + * @var $group Model\Universe\GroupModel + */ + $group = Model\Universe\AbstractUniverseModel::getNew('GroupModel'); + $group->storeDogmaAttributes = $storeDogmaAttributes; + $group->loadById($groupId); + $info = $group->loadTypesData($offset, $length); + } + + return $info; } // system search index methods ==================================================================================== @@ -146,10 +161,11 @@ class Universe extends Controller { $system = Model\Universe\AbstractUniverseModel::getNew('SystemModel'); $indexData = []; foreach($systemIds as $systemId){ - $system->getById($systemId); + $system->getById($systemId, 0); if($hashKeyId = $system->getHashKey()){ $indexData[$hashKeyId] = $system->getData(); } + $system->reset(); // offset must increase otherwise we get a endless loop // -> see /setup ajax build loop function $offset++; @@ -165,12 +181,14 @@ class Universe extends Controller { /** * get systemIds for all systems + * @param bool $ignoreCache * @return array * @throws \Exception */ - public function getSystemIds() : array { + public function getSystemIds(bool $ignoreCache = false) : array { $f3 = $this->getF3(); - if( !$f3->exists(self::SESSION_KEY_SYSTEM_IDS, $systemIds) ){ + $systemIds = []; + if($ignoreCache || !$f3->exists(self::SESSION_KEY_SYSTEM_IDS, $systemIds)){ /** * @var $system Model\Universe\SystemModel */ @@ -184,7 +202,7 @@ class Universe extends Controller { } } - return (array)$systemIds; + return $systemIds ? : []; } /** @@ -225,20 +243,20 @@ class Universe extends Controller { * @return null|\stdClass * @throws \Exception */ - public function getSystemData(int $systemId){ + public function getSystemData(int $systemId) : ?\stdClass { $data = null; if($systemId){ // ...check index for data $cacheKeyRow = Model\Universe\AbstractUniverseModel::generateHashKeyRow('system', $systemId); - $data = $this->get($cacheKeyRow); - if(!$data){ + if(!$data = $this->get($cacheKeyRow)){ // .. try to build index /** * @var $system Model\Universe\SystemModel */ $system = Model\Universe\AbstractUniverseModel::getNew('SystemModel'); - $system->getById($systemId); - $data = $system->buildIndex(); + if($system->getById($systemId)){ + $data = $system->buildIndex(); + } } } return $data; @@ -249,7 +267,7 @@ class Universe extends Controller { * @param string $cacheKey * @return null|\stdClass */ - private function get(string $cacheKey){ + private function get(string $cacheKey) : ?\stdClass { $data = null; if($this->getF3()->exists($cacheKey,$value)) { if(is_string($value) && strpos($value, Model\Universe\AbstractUniverseModel::CACHE_KEY_PREFIX) === 0) { diff --git a/app/main/controller/controller.php b/app/main/controller/controller.php index ee020a51..7433ec18 100644 --- a/app/main/controller/controller.php +++ b/app/main/controller/controller.php @@ -16,6 +16,7 @@ use lib\db\SQL; use lib\Resource; use lib\Monolog; use lib\Util; +use Model\AbstractModel; use Model\Pathfinder; use DB; @@ -381,13 +382,14 @@ class Controller { } /** - * get current character data from session - * @return array + * get current character from session data + * @param int $ttl + * @return Pathfinder\CharacterModel|null * @throws \Exception */ - public function getSessionCharacterData() : array { - $data = []; - if($user = $this->getUser()){ + protected function getSessionCharacter(int $ttl = AbstractModel::DEFAULT_SQL_TTL) : ?Pathfinder\CharacterModel { + $character = null; + if($user = $this->getUser($ttl)){ $header = self::getRequestHeaders(); $requestedCharacterId = (int)$header['Pf-Character']; if( !$this->getF3()->get('AJAX') ){ @@ -400,10 +402,10 @@ class Controller { } } - $data = $user->getSessionCharacterData($requestedCharacterId); + $character = $user->getSessionCharacter($requestedCharacterId, $ttl); } - return $data; + return $character; } /** @@ -412,24 +414,8 @@ class Controller { * @return Pathfinder\CharacterModel|null * @throws \Exception */ - public function getCharacter(int $ttl = 0) : ?Pathfinder\CharacterModel { - $character = null; - - if(!empty($characterData = $this->getSessionCharacterData())){ - /** - * @var $characterModel Pathfinder\CharacterModel - */ - $characterModel = Pathfinder\AbstractPathfinderModel::getNew('CharacterModel'); - $characterModel->getById((int)$characterData['ID'], $ttl); - if( - !$characterModel->dry() && - $characterModel->hasUserCharacter() - ){ - $character = &$characterModel; - } - } - - return $character; + public function getCharacter(int $ttl = AbstractModel::DEFAULT_SQL_TTL) : ?Pathfinder\CharacterModel { + return $this->getSessionCharacter($ttl); } /** @@ -438,7 +424,7 @@ class Controller { * @return Pathfinder\UserModel|null * @throws \Exception */ - public function getUser($ttl = 0) : ?Pathfinder\UserModel { + public function getUser($ttl = AbstractModel::DEFAULT_SQL_TTL) : ?Pathfinder\UserModel { $user = null; if($this->getF3()->exists(Api\User::SESSION_KEY_USER_ID, $userId)){ @@ -452,7 +438,7 @@ class Controller { !$userModel->dry() && $userModel->hasUserCharacters() ){ - $user = &$userModel; + $user = $userModel; } } @@ -534,10 +520,11 @@ class Controller { * @throws \Exception */ public function getEveServerStatus(\Base $f3){ + $ttl = 60; $esiStatusVersion = 'latest'; $cacheKey = 'eve_server_status'; - if( !$f3->exists($cacheKey, $return) ){ + if(!$exists = $f3->exists($cacheKey, $return)){ $return = (object) []; $return->error = []; @@ -593,10 +580,10 @@ class Controller { // find top status $status = 'OK'; $color = 'green'; - foreach($apiStatus['status'] as $statusData){ + foreach($apiStatus['status'] as &$statusData){ if('red' == $statusData['status']){ $status = 'unstable'; - $color = $statusData['status']; + $color = $statusData['status'] = 'orange'; // red is already in use for fatal API errors (e.g. no response at all, or offline) break; } if('yellow' == $statusData['status']){ @@ -613,11 +600,15 @@ class Controller { } if(empty($return->error)){ - $f3->set($cacheKey, $return, 60); + $f3->set($cacheKey, $return, $ttl); } } } + if(empty($return->error)){ + $f3->expire(Config::ttlLeft($exists, $ttl)); + } + echo json_encode($return); } diff --git a/app/main/controller/logcontroller.php b/app/main/controller/logcontroller.php index ebe47a49..1fc900b3 100644 --- a/app/main/controller/logcontroller.php +++ b/app/main/controller/logcontroller.php @@ -118,13 +118,13 @@ class LogController extends \Prefab { $updateSql = array_map($updateRule, $columnsForUpdate); $sql = "INSERT DELAYED INTO - activity_log (" . implode(', ', $columnsQuoted) . ") values( - " . implode(', ', $placeholder) . " - ) - ON DUPLICATE KEY UPDATE - updated = NOW(), - " . implode(', ', $updateSql) . " - "; + activity_log (" . implode(', ', $columnsQuoted) . ") VALUES( + " . implode(', ', $placeholder) . " + ) + ON DUPLICATE KEY UPDATE + updated = NOW(), + " . implode(', ', $updateSql) . " + "; $db->exec($sql, $args); } diff --git a/app/main/controller/setup.php b/app/main/controller/setup.php index a38eeba4..b7ebb57a 100644 --- a/app/main/controller/setup.php +++ b/app/main/controller/setup.php @@ -67,7 +67,6 @@ class Setup extends Controller { 'Model\Pathfinder\MapTypeModel', 'Model\Pathfinder\SystemTypeModel', 'Model\Pathfinder\SystemStatusModel', - 'Model\Pathfinder\SystemNeighbourModel', 'Model\Pathfinder\RightModel', 'Model\Pathfinder\RoleModel', 'Model\Pathfinder\StructureModel', @@ -105,19 +104,27 @@ class Setup extends Controller { 'UNIVERSE' => [ 'info' => [], 'models' => [ + 'Model\Universe\DogmaAttributeModel', + 'Model\Universe\TypeAttributeModel', 'Model\Universe\TypeModel', 'Model\Universe\GroupModel', 'Model\Universe\CategoryModel', 'Model\Universe\FactionModel', + 'Model\Universe\AllianceModel', + 'Model\Universe\CorporationModel', + 'Model\Universe\RaceModel', + 'Model\Universe\StationModel', 'Model\Universe\StructureModel', - 'Model\Universe\WormholeModel', 'Model\Universe\StargateModel', 'Model\Universe\StarModel', 'Model\Universe\PlanetModel', 'Model\Universe\SystemModel', 'Model\Universe\ConstellationModel', 'Model\Universe\RegionModel', - 'Model\Universe\SystemStaticModel' + 'Model\Universe\SystemNeighbourModel', + 'Model\Universe\SystemStaticModel', + 'Model\Universe\SovereigntyMapModel', + 'Model\Universe\FactionWarSystemModel' ] ] ]; @@ -1665,18 +1672,111 @@ class Setup extends Controller { * @var $categoryUniverseModel Universe\CategoryModel */ $categoryUniverseModel = Universe\AbstractUniverseModel::getNew('CategoryModel'); - $categoryUniverseModel->getById(65, 0); - $structureCount = $categoryUniverseModel->getTypesCount(false); + $categoryUniverseModel->getById(Config::ESI_CATEGORY_STRUCTURE_ID, 0); + $groupsCountStructure = $categoryUniverseModel->getGroupsCount(false); + $typesCountStructure = $categoryUniverseModel->getTypesCount(false); - $categoryUniverseModel->getById(6, 0); - $shipCount = $categoryUniverseModel->getTypesCount(false); + $categoryUniverseModel->getById(Config::ESI_CATEGORY_SHIP_ID, 0); + $groupsCountShip = $categoryUniverseModel->getGroupsCount(false); + $typesCountShip = $categoryUniverseModel->getTypesCount(false); /** - * @var $systemNeighbourModel Pathfinder\SystemNeighbourModel + * @var $groupUniverseModel Universe\GroupModel */ - $systemNeighbourModel = Pathfinder\AbstractPathfinderModel::getNew('SystemNeighbourModel'); + + $groupUniverseModel = Universe\AbstractUniverseModel::getNew('GroupModel'); + $groupUniverseModel->getById(Config::ESI_GROUP_WORMHOLE_ID, 0); + $wormholeCount = $groupUniverseModel->getTypesCount(false); + + /** + * @var $systemNeighbourModel Universe\SystemNeighbourModel + */ + $systemNeighbourModel = Universe\AbstractUniverseModel::getNew('SystemNeighbourModel'); + + /** + * @var $systemStaticModel Universe\SystemStaticModel + */ + $systemStaticModel = Universe\AbstractUniverseModel::getNew('SystemStaticModel'); + + if(empty($systemCountAll = count(($universeController = new UniverseController())->getSystemIds(true)))){ + // no systems found in 'universe' DB. Clear potential existing system cache + $universeController->clearSystemsIndex(); + } + + $sum = function(int $carry, int $value) : int { + return $carry + $value; + }; $indexInfo = [ + 'Wormholes' => [ + 'task' => [ + [ + 'action' => 'buildIndex', + 'label' => 'Import', + 'icon' => 'fa-sync', + 'btn' => 'btn-primary' + ] + ], + 'label' => 'Wormholes data', + 'countBuild' => $wormholeCount, + 'countAll' => count(Universe\GroupModel::getUniverseGroupTypes(Config::ESI_GROUP_WORMHOLE_ID)), + 'tooltip' => 'import all wormhole types (e.g. L031) from ESI. Runtime: ~25s' + ], + 'Structures' => [ + 'task' => [ + [ + 'action' => 'buildIndex', + 'label' => 'Import', + 'icon' => 'fa-sync', + 'btn' => 'btn-primary' + ] + ], + 'label' => 'Structures data', + 'countBuild' => $groupsCountStructure, + 'countAll' => count(Universe\CategoryModel::getUniverseCategoryGroups(Config::ESI_CATEGORY_STRUCTURE_ID)), + 'tooltip' => 'import all structure types (e.g. Citadels) from ESI. Runtime: ~15s', + 'subCount' => [ + 'countBuild' => $typesCountStructure, + 'countAll' => array_reduce(array_map('count', Universe\CategoryModel::getUniverseCategoryTypes(Config::ESI_CATEGORY_STRUCTURE_ID)), $sum, 0), + ] + ], + 'Ships' => [ + 'task' => [ + [ + 'action' => 'buildIndex', + 'label' => 'Import', + 'icon' => 'fa-sync', + 'btn' => 'btn-primary' + ] + ], + 'label' => 'Ships data', + 'countBuild' => $groupsCountShip, + 'countAll' => count(Universe\CategoryModel::getUniverseCategoryGroups(Config::ESI_CATEGORY_SHIP_ID)), + 'tooltip' => 'import all ships from ESI. Runtime: ~2min', + 'subCount' => [ + 'countBuild' => $typesCountShip, + 'countAll' => array_reduce(array_map('count', Universe\CategoryModel::getUniverseCategoryTypes(Config::ESI_CATEGORY_SHIP_ID)), $sum, 0), + ] + ], + 'SystemStatic' => [ + 'task' => [ + [ + 'action' => 'buildIndex', + 'label' => 'Import', + 'icon' => 'fa-sync', + 'btn' => 'btn-primary' + ] + ], + 'label' => 'Wormhole statics data', + 'countBuild' => $systemStaticModel->getRowCount(), + 'countAll' => 3772, + 'tooltip' => 'import all static wormholes for systems. Runtime: ~25s' + ], + [ + 'label' => 'Build search index', + 'icon' => 'fa-search', + 'tooltip' => 'Search indexes are build from static EVE universe data (e.g. systems, stargate connections,…). Re-build if underlying data was updated.' + ], 'Systems' => [ 'task' => [ [ @@ -1691,81 +1791,36 @@ class Setup extends Controller { 'btn' => 'btn-primary' ] ], - 'label' => 'build systems index', - 'countBuild' => count((new UniverseController())->getSystemsIndex()), - 'countAll' => count((new UniverseController())->getSystemIds()), - 'tooltip' => 'build up a static search index over all systems found on DB. Do not refresh page until import is complete (check progress)! Runtime: ~5min' - ], - 'Structures' => [ - 'task' => [ - [ - 'action' => 'buildIndex', - 'label' => 'Import', - 'icon' => 'fa-sync', - 'btn' => 'btn-primary' - ] - ], - 'label' => 'import structures data', - 'countBuild' => $structureCount, - 'countAll' => (int)$f3->get('REQUIREMENTS.DATA.STRUCTURES'), - 'tooltip' => 'import all structure types (e.g. Citadels) from ESI. Runtime: ~15s' - ], - 'Ships' => [ - 'task' => [ - [ - 'action' => 'buildIndex', - 'label' => 'Import', - 'icon' => 'fa-sync', - 'btn' => 'btn-primary' - ] - ], - 'label' => 'import ships data', - 'countBuild' => $shipCount, - 'countAll' => (int)$f3->get('REQUIREMENTS.DATA.SHIPS'), - 'tooltip' => 'import all ships types from ESI. Runtime: ~2min' + 'label' => 'Systems data index', + 'countBuild' => count($universeController->getSystemsIndex()), + 'countAll' => $systemCountAll, + 'tooltip' => 'Build up a static search index over all systems, found on DB. Runtime: ~5min' ], 'SystemNeighbour' => [ 'task' => [ [ + 'action' => 'clearIndex', + 'label' => 'Clear', + 'icon' => 'fa-trash', + 'btn' => 'btn-danger' + ],[ 'action' => 'buildIndex', 'label' => 'Build', 'icon' => 'fa-sync', 'btn' => 'btn-primary' ] ], - 'label' => 'build neighbour index', - 'countBuild' => $f3->DB->getDB('PF')->getRowCount($systemNeighbourModel->getTable()), + 'label' => 'Systems neighbour index', + 'countBuild' => $systemNeighbourModel->getRowCount(), 'countAll' => (int)$f3->get('REQUIREMENTS.DATA.NEIGHBOURS'), - 'tooltip' => 'build up a static search index for route search. This is used as fallback in case ESI is down. Runtime: ~30s' - - ], - // All following rows become deprecated - /* - 'WormholeModel' => [ - 'task' => [ - [ - 'action' => 'exportTable', - 'label' => 'Export', - 'icon' => 'fa-download', - 'btn' => 'btn-default' - ],[ - 'action' => 'importTable', - 'label' => 'Import', - 'icon' => 'fa-upload', - 'btn' => 'btn-primary' - ] - ], - 'label' => 'wormhole', - 'countBuild' => $f3->DB->getDB('PF')->getRowCount($wormholeModel->getTable()), - 'countAll' => 89 + 'tooltip' => 'Build up a static search index for route search. This is used as fallback in case ESI is down. Runtime: ~10s' ] - */ ]; }else{ $indexInfo = [ - 'SystemNeighbour' => [ - 'task' => [], - 'label' => 'Fix database errors first!' + [ + 'label' => 'Fix database errors first!', + 'class' => 'txt-color-danger text-center' ] ]; } diff --git a/app/main/cron/abstractcron.php b/app/main/cron/abstractcron.php index d98c7706..2a758883 100644 --- a/app/main/cron/abstractcron.php +++ b/app/main/cron/abstractcron.php @@ -10,13 +10,21 @@ namespace cron; abstract class AbstractCron { - // default max_execution_time for cronJobs + /** + * default max_execution_time for cronJobs // -> should be less then execution period + */ const DEFAULT_MAX_EXECUTION_TIME = 50; /** - * set max execution time for cronjobs - * -> Default CLI execution time is "0" -> infinite! + * default threshold time in seconds before a running script (e.g. a large loop) should stop + * -> so there is some time or e.g. logging,... left + */ + const DEFAULT_EXECUTION_TIME_THRESHOLD = 3; + + /** + * set max execution time for cronJbs + * -> Default CLI execution time is 0 == infinite! * php.ini settings are ignored! http://php.net/manual/en/info.configuration.php#ini.max-execution-time * @param int $time */ @@ -24,4 +32,33 @@ abstract class AbstractCron { ini_set('max_execution_time', $time); } + /** + * get max execution time + * -> 0 means == infinite! + * @return int + */ + protected function getMaxExecutionTime() : int { + return (int)ini_get('max_execution_time'); + } + + /** + * checks execution time of a "long" running script + * -> returns false if execution time is close to maxExecutionTime + * @param float $timeTotalStart + * @param float|null $timeCheck + * @param int $timeThreshold + * @return bool + */ + protected function isExecutionTimeLeft(float $timeTotalStart, float $timeCheck = null, int $timeThreshold = self::DEFAULT_EXECUTION_TIME_THRESHOLD) : bool { + $timeLeft = true; + if($timeTotalMax = $this->getMaxExecutionTime()){ + $timeTotalMaxThreshold = $timeTotalStart + $timeTotalMax - $timeThreshold; + $timeCheck = $timeCheck ? : microtime(true); + if($timeCheck >= $timeTotalMaxThreshold){ + $timeLeft = false; + } + } + return $timeLeft; + } + } \ No newline at end of file diff --git a/app/main/cron/characterupdate.php b/app/main/cron/characterupdate.php index b1cb2055..6208d0e0 100644 --- a/app/main/cron/characterupdate.php +++ b/app/main/cron/characterupdate.php @@ -65,7 +65,7 @@ class CharacterUpdate extends AbstractCron { */ if(is_object($characterLog->characterId)){ if($accessToken = $characterLog->characterId->getAccessToken()){ - if($this->isOnline($accessToken)){ + if($characterLog->characterId->isOnline($accessToken)){ // force characterLog as "updated" even if no changes were made $characterLog->touch('updated'); $characterLog->save(); diff --git a/app/main/cron/universe.php b/app/main/cron/universe.php index b39d06a7..1dc79fb1 100644 --- a/app/main/cron/universe.php +++ b/app/main/cron/universe.php @@ -13,6 +13,7 @@ use Model; class Universe extends AbstractCron { const LOG_TEXT = '%s type: %s %s/%s peak: %s total: %s msg: %s'; + const LOG_TEXT_SOV_FW = '%s %4s/%-4s checked, %s peak, %s total, %4s updated [%4s sovChanges, %4s fwChanges], msg: %s'; /** @@ -133,10 +134,11 @@ class Universe extends AbstractCron { * @param float $timeTotalStart */ private function echoLoaded(int $importCount, int $id, float $timeLoopStart, float $timeTotalStart){ + $time = microtime(true); echo '[' . date('H:i:s') . '] loaded ' . str_pad('', strlen($importCount), ' ') . ' id: ' . $this->formatIdValue($id) . ' memory: ' . $this->formatMemoryValue(memory_get_usage()) . - ' time: ' . $this->formatSeconds(microtime(true) - $timeLoopStart) . - ' total: ' . $this->formatSeconds(microtime(true) - $timeTotalStart) . PHP_EOL; + ' time: ' . $this->formatSeconds($time - $timeLoopStart) . + ' total: ' . $this->formatSeconds($time - $timeTotalStart) . PHP_EOL; $this->echoFlush(); } @@ -169,8 +171,8 @@ class Universe extends AbstractCron { $msg = ''; $ids = []; + $importCount = 0; $count = 0; - $importCount = []; $modelClass = ''; $setupModel = function(Model\Universe\AbstractUniverseModel &$model, int $id){}; @@ -193,12 +195,51 @@ class Universe extends AbstractCron { $model->loadStargatesData(); }; break; + case 'station': + $ids = $f3->ccpClient()->getUniverseSystems(); + $modelClass = 'SystemModel'; + $setupModel = function(Model\Universe\SystemModel &$model, int $id){ + if($model->getById($id)){ + $model->loadStationsData(); + }else{ + echo 'NOT VALID ' . $id . PHP_EOL; + die(); + } + }; + break; + case 'sovereignty': + // load sovereignty map data. Systems must be present first! + $sovData = $f3->ccpClient()->getSovereigntyMap(); + $ids = !empty($sovData = $sovData['map']) ? array_keys($sovData): []; + $modelClass = 'SystemModel'; + $setupModel = function(Model\Universe\SystemModel &$model, int $id) use ($sovData) { + if($model->getById($id)){ + $model->updateSovereigntyData($sovData[$id]); + }else{ + echo 'NOT VALID ' . $id . PHP_EOL; + die(); + } + }; + break; + case 'faction_war_systems': + $fwSystems = $f3->ccpClient()->getFactionWarSystems(); + $ids = !empty($fwSystems = $fwSystems['systems']) ? array_keys($fwSystems): []; + $modelClass = 'SystemModel'; + $setupModel = function(Model\Universe\SystemModel &$model, int $id) use ($fwSystems) { + if($model->getById($id)){ + $model->updateFactionWarData($fwSystems[$id]); + }else{ + echo 'NOT VALID ' . $id . PHP_EOL; + die(); + } + }; + break; case 'index_system': // setup system index, Systems must be present first! $ids = $f3->ccpClient()->getUniverseSystems(); $modelClass = 'SystemModel'; $setupModel = function(Model\Universe\SystemModel &$model, int $id){ - $model->getById($id); // no loadById() here! would take "forever" when system not exists and build up first... + $model->getById($id); // no loadById() here! would take "forever" when system not exists and must be build up first... $model->buildIndex(); }; break; @@ -216,6 +257,7 @@ class Universe extends AbstractCron { sort($ids, SORT_NUMERIC); $ids = array_slice($ids, $offset, $length); $importCount = count($ids); + $count = 0; $this->echoInfo($total, $offset, $importCount, $ids); $this->echoStart(); @@ -237,9 +279,83 @@ class Universe extends AbstractCron { // Log -------------------------------------------------------------------------------------------------------- $log = new \Log('cron_' . __FUNCTION__ . '.log'); - $log->write( sprintf(self::LOG_TEXT, __FUNCTION__, $type, + $log->write(sprintf(self::LOG_TEXT, __FUNCTION__, $type, $this->formatCounterValue($count), $importCount, $this->formatMemoryValue(memory_get_peak_usage ()), - $this->formatSeconds(microtime(true) - $timeTotalStart), $msg) ); + $this->formatSeconds(microtime(true) - $timeTotalStart), $msg)); + } + + /** + * update Sovereignty system data from ESI + * -> this updates Faction warfare data as well + * >> php index.php "/cron/updateSovereigntyData" + * @param \Base $f3 + * @throws \Exception + */ + function updateSovereigntyData(\Base $f3){ + $this->setMaxExecutionTime(); + $timeTotalStart = microtime(true); + $msg = ''; + + /** + * @var $system Model\Universe\SystemModel + */ + $system = Model\Universe\AbstractUniverseModel::getNew('SystemModel'); + + $sovData = $f3->ccpClient()->getSovereigntyMap(); + $fwSystems = $f3->ccpClient()->getFactionWarSystems(); + $fwSystems = $fwSystems['systems']; + $ids = !empty($sovData = $sovData['map']) ? array_keys($sovData): []; + sort($ids, SORT_NUMERIC); + $importCount = count($ids); + $count = 0; + + $changes = []; + foreach($ids as $id){ + $count++; + + // skip wormhole systems -> can not have sov data + // -> even though they are returned from sovereignty/map endpoint?! + if( + $system->getById($id, 0) && + strpos($system->security, 'C') === false + ){ + if($changedSovData = $system->updateSovereigntyData($sovData[$id])){ + $changes['sovereignty'][] = $id; + } + + $changedFwData = false; + if(is_array($fwSystems[$id])){ + if($changedFwData = $system->updateFactionWarData($fwSystems[$id])){ + $changes['factionWarfare'][] = $id; + } + } + + if($changedSovData || $changedFwData){ + $system->buildIndex(); + } + } + $system->reset(); + + // stop loop if runtime gets close to "max_execution_time" + // -> we need some time for writing *.log file + if(!$this->isExecutionTimeLeft($timeTotalStart)){ + $msg = 'Script execution stopped due to "max_execution_time" limit reached'; + // TODO store current loop index and use it as new "offset" for next call + break; + } + } + + $changedIds = array_reduce($changes, function(array $reducedIds, array $changedIds) : array { + return array_unique(array_merge($reducedIds, $changedIds)); + }, []); + + // Log ------------------------ + $log = new \Log('cron_' . __FUNCTION__ . '.log'); + $log->write(sprintf(self::LOG_TEXT_SOV_FW, __FUNCTION__, + $count, $importCount, $this->formatMemoryValue(memory_get_peak_usage ()), + $this->formatSeconds(microtime(true) - $timeTotalStart), + count($changedIds), count($changes['sovereignty'] ? : []), count($changes['factionWarfare'] ? : []), + $msg)); } /** @@ -251,9 +367,12 @@ class Universe extends AbstractCron { */ function updateUniverseSystems(\Base $f3){ $this->setMaxExecutionTime(); - - $system = Model\Universe\AbstractUniverseModel::getNew('SystemModel'); - $systems = $system->find( null, ['order' => 'updated', 'limit' => 2]); + /** + * @var $systemModel Model\Universe\SystemModel + * @var $system Model\Universe\SystemModel + */ + $systemModel = Model\Universe\AbstractUniverseModel::getNew('SystemModel'); + $systems = $systemModel->find( null, ['order' => 'updated', 'limit' => 2]); if($systems){ foreach ($systems as $system){ $system->updateModel(); diff --git a/app/main/lib/PriorityCacheStore.php b/app/main/lib/PriorityCacheStore.php new file mode 100644 index 00000000..740d0a32 --- /dev/null +++ b/app/main/lib/PriorityCacheStore.php @@ -0,0 +1,124 @@ + truncate store after 10 inserts. Max store entries: + * DEFAULT_ENTRY_LIMIT + DEFAULT_CLEANUP_INTERVAL - 1 + */ + const DEFAULT_CLEANUP_INTERVAL = 10; + + /** + * @var int + */ + protected $entryLimit; + + /** + * @var int + */ + protected $cleanupInterval; + + /** + * @var array + */ + protected $store; + + /** + * @var \SplPriorityQueue + */ + protected $priorityQueue; + + /** + * @var int + */ + protected $priority = 0; + + /** + * PriorityCacheStore constructor. + * @param int $entryLimit + * @param int $cleanupInterval + */ + function __construct(int $entryLimit = self::DEFAULT_ENTRY_LIMIT, int $cleanupInterval = self::DEFAULT_CLEANUP_INTERVAL){ + $this->cleanupInterval = $cleanupInterval; + $this->entryLimit = $entryLimit; + $this->store = []; + $this->priorityQueue = new \SplPriorityQueue (); + $this->priorityQueue->setExtractFlags(\SplPriorityQueue::EXTR_BOTH); + } + + /** + * @param $key + * @param $data + */ + public function set($key, $data){ + if(!$this->exists($key)){ + $this->priorityQueue->insert($key, $this->priority--); + } + + $this->store[$key] = $data; + + // check cleanup interval and cleanup Store + $this->cleanupInterval(); + } + + /** + * @param $key + * @return mixed|null + */ + public function get($key){ + return $this->exists($key) ? $this->store[$key] : null; + } + + /** + * @param $key + * @return bool + */ + public function exists($key){ + return isset($this->store[$key]); + } + + public function cleanupInterval() : void { + if( + !$this->priorityQueue->isEmpty() && $this->cleanupInterval && + ($this->priorityQueue->count() % $this->cleanupInterval === 0) + ){ + $this->cleanup(); + } + } + + public function cleanup(){ + while( + $this->entryLimit < $this->priorityQueue->count() && + $this->priorityQueue->valid() + ){ + if($this->exists($key = $this->priorityQueue->extract()['data'])){ + unset($this->store[$key]); + } + } + } + + public function clear(){ + $limit = $this->entryLimit; + $this->entryLimit = 0; + $this->cleanup(); + // restore entryLimit for next data + $this->entryLimit = $limit; + } + + /** + * @return string + */ + public function __toString(){ + return 'Store count: ' . count($this->store) . ' priorityQueue count: ' . $this->priorityQueue->count(); + } +} \ No newline at end of file diff --git a/app/main/lib/config.php b/app/main/lib/config.php index 1ce9ee9d..c87db59b 100644 --- a/app/main/lib/config.php +++ b/app/main/lib/config.php @@ -82,6 +82,30 @@ class Config extends \Prefab { */ const DOWNTIME_BUFFER = 1; + // ================================================================================================================ + // ESI API id´s + // ================================================================================================================ + + /** + * ESI categoryId or 'Structure's + */ + const ESI_CATEGORY_STRUCTURE_ID = 65; + + /** + * ESI categoryId or 'Ship's + */ + const ESI_CATEGORY_SHIP_ID = 6; + + /** + * ESI groupId or 'Wormhole's + */ + const ESI_GROUP_WORMHOLE_ID = 988; + + /** + * ESI dogmaAttributeId for 'scanWormholeStrength's + */ + const ESI_DOGMA_ATTRIBUTE_SCANWHSTRENGTH_ID = 1908; + /** * error message for missing Composer dependency class */ @@ -166,7 +190,7 @@ class Config extends \Prefab { * @return array|null */ protected function getAllEnvironmentData(\Base $f3){ - if( !$f3->exists(self::HIVE_KEY_ENVIRONMENT, $environmentData) ){ + if(!$f3->exists(self::HIVE_KEY_ENVIRONMENT, $environmentData)){ $environmentData = $this->setAllEnvironmentData($f3); } @@ -320,7 +344,7 @@ class Config extends \Prefab { if($config['SCHEME'] == 'mysql'){ $options[\PDO::MYSQL_ATTR_COMPRESS] = true; $options[\PDO::MYSQL_ATTR_INIT_COMMAND] = implode(',', [ - "SET NAMES " . strtolower(str_replace('-','', $f3->ENCODING)), + "SET NAMES " . self::getRequiredDbVars($f3, $config['SCHEME'])['CHARACTER_SET_CONNECTION'] . " COLLATE " . self::getRequiredDbVars($f3, $config['SCHEME'])['COLLATION_CONNECTION'], "@@session.time_zone = '+00:00'", "@@session.default_storage_engine = " . self::getRequiredDbVars($f3, $config['SCHEME'])['DEFAULT_STORAGE_ENGINE'] ]); @@ -616,4 +640,18 @@ class Config extends \Prefab { return $format; } + static function ttlLeft($fromExists, int $ttlMax) : int { + $ttlMax = max($ttlMax, 0); + if($fromExists){ + // == true || array + if(is_array($fromExists)){ + return max(min((int)ceil(round(array_sum($fromExists) - microtime(true), 4)), $ttlMax), 0); + }else{ + return 0; + } + }else{ + // == false + return $ttlMax; + } + } } \ No newline at end of file diff --git a/app/main/lib/db/Pool.php b/app/main/lib/db/Pool.php index a3cbd545..2a415f92 100644 --- a/app/main/lib/db/Pool.php +++ b/app/main/lib/db/Pool.php @@ -110,8 +110,8 @@ class Pool extends \Prefab { $schema = new Schema($db); if(!in_array($newDbName, $schema->getDatabases())){ $db->exec("CREATE DATABASE IF NOT EXISTS - `" . $newDbName . "` DEFAULT CHARACTER SET utf8 - COLLATE utf8_general_ci;"); + `" . $newDbName . "` DEFAULT CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci;"); $db->exec("USE `" . $newDbName . "`"); // check if DB create was successful diff --git a/app/main/lib/util.php b/app/main/lib/util.php index d0b4b32e..8d031de6 100644 --- a/app/main/lib/util.php +++ b/app/main/lib/util.php @@ -51,12 +51,30 @@ class Util { return $return; } + /** + * transforms array with assoc. arrays as values + * into assoc. array where $key column data is used for its key + * @param array $array + * @param string $key + * @param bool $unsetKey + * @return array + */ + static function arrayGetBy(array $array, string $key, bool $unsetKey = true) : array { + // we can remove $key from nested arrays + return array_map(function($val) use ($key, $unsetKey) : array { + if($unsetKey){ + unset($val[$key]); + } + return $val; + }, array_column($array, null, $key)); + } + /** * checks whether an array is associative or not (sequential) * @param mixed $array * @return bool */ - static function is_assoc($array): bool { + static function is_assoc($array) : bool { $isAssoc = false; if( is_array($array) && @@ -109,7 +127,7 @@ class Util { * @param int $maxHideChars * @return string */ - static function obscureString(string $string, int $maxHideChars = 10): string { + static function obscureString(string $string, int $maxHideChars = 10) : string { $formatted = ''; $length = mb_strlen((string)$string); if($length > 0){ @@ -125,7 +143,7 @@ class Util { * @param array $scopes * @return string */ - static function getHashFromScopes($scopes){ + static function getHashFromScopes($scopes) : string { $scopes = (array)$scopes; sort($scopes); return md5(serialize($scopes)); diff --git a/app/main/model/abstractmodel.php b/app/main/model/abstractmodel.php index 7102f198..e93af259 100644 --- a/app/main/model/abstractmodel.php +++ b/app/main/model/abstractmodel.php @@ -11,6 +11,7 @@ namespace Model; use DB\Cortex; use DB\CortexCollection; use DB\SQL\Schema; +use lib\Util; use lib\logging; use Controller; use Exception\ValidationException; @@ -44,6 +45,14 @@ abstract class AbstractModel extends Cortex { */ protected $addStaticFields = true; + /** + * enables table truncate + * -> see truncate(); + * -> CAUTION! if set to true truncate() will clear ALL rows! + * @var bool + */ + protected $allowTruncate = false; + /** * enables change for "active" column * -> see setActive(); @@ -89,6 +98,23 @@ abstract class AbstractModel extends Cortex { */ const DEFAULT_CACHE_TTL = 120; + /** + * default TTL or temp table data read from *.csv file + * -> used during data import + */ + const DEFAULT_CACHE_CSV_TTL = 120; + + /** + * cache key prefix name for "full table" indexing + * -> used e.g. for a "search" index; or "import" index for *.csv imports + */ + const CACHE_KEY_PREFIX = 'INDEX'; + + /** + * cache key name for temp data import from *.csv files per table + */ + const CACHE_KEY_CSV_PREFIX = 'CSV'; + /** * default TTL for SQL query cache */ @@ -495,7 +521,7 @@ abstract class AbstractModel extends Cortex { * @return bool */ public function getById(int $id, int $ttl = self::DEFAULT_SQL_TTL, bool $isActive = true) : bool { - return $this->getByForeignKey('id', $id, ['limit' => 1], $ttl, $isActive); + return $this->getByForeignKey($this->primary, $id, ['limit' => 1], $ttl, $isActive); } /** @@ -508,10 +534,7 @@ abstract class AbstractModel extends Cortex { * @return bool */ public function getByForeignKey(string $key, $value, array $options = [], int $ttl = 0, bool $isActive = true) : bool { - $filters = []; - if($this->exists($key)){ - $filters[] = [$key . ' = :' . $key, ':' . $key => $value]; - } + $filters = [self::getFilter($key, $value)]; if($isActive && $this->exists('active')){ $filters[] = self::getFilter('active', true); @@ -658,6 +681,24 @@ abstract class AbstractModel extends Cortex { return true; } + /** + * get row count in this table + * @return int + */ + public function getRowCount() : int { + return is_object($this->db) ? $this->db->getRowCount($this->getTable()) : 0; + } + + /** + * truncate all table rows + * -> Use with Caution!!! + */ + public function truncate(){ + if($this->allowTruncate && is_object($this->db)){ + $this->db->exec("TRUNCATE " . $this->getTable()); + } + } + /** * format dateTime column * @param $column @@ -716,43 +757,78 @@ abstract class AbstractModel extends Cortex { } /** - * import table data from a *.csv file - * @return array|bool + * read *.csv file for a $table name + * -> 'group' by $getByKey column name and return array + * @param string $table + * @param string $getByKey + * @return array */ - public function importData(){ - $status = false; + public static function getCSVData(string $table, string $getByKey = 'id') : array { + $hashKeyTableCSV = static::generateHashKeyTable($table, static::CACHE_KEY_PREFIX . '_' . self::CACHE_KEY_CSV_PREFIX); + + if( + !self::getF3()->exists($hashKeyTableCSV, $tableData) && + !empty($tableData = Util::arrayGetBy(self::loadCSV($table), $getByKey, false)) + ){ + self::getF3()->set($hashKeyTableCSV, $tableData, self::DEFAULT_CACHE_CSV_TTL); + } + + return $tableData; + } + + /** + * load data from *.csv file + * @param string $fileName + * @return array + */ + protected static function loadCSV(string $fileName) : array { + $tableData = []; // rtrim(); for arrays (removes empty values) from the end - $rtrim = function($array = [], $lengthMin = false){ + $rtrim = function($array = [], $lengthMin = false) : array { $length = key(array_reverse(array_diff($array, ['']), 1))+1; $length = $length < $lengthMin ? $lengthMin : $length; return array_slice($array, 0, $length); }; - if(static::$enableDataImport){ - $filePath = $this->getF3()->get('EXPORT') . 'csv/' . $this->getTable() . '.csv'; - + if($fileName){ + $filePath = self::getF3()->get('EXPORT') . 'csv/' . $fileName . '.csv'; if(is_file($filePath)){ $handle = @fopen($filePath, 'r'); $keys = array_map('lcfirst', fgetcsv($handle, 0, ';')); $keys = $rtrim($keys); if(count($keys) > 0){ - $tableData = []; while (!feof($handle)) { $tableData[] = array_combine($keys, $rtrim(fgetcsv($handle, 0, ';'), count($keys))); } - // import row data - $status = $this->importStaticData($tableData); - $this->getF3()->status(202); }else{ - $this->getF3()->error(500, 'File could not be read'); + self::getF3()->error(500, 'File could not be read'); } }else{ - $this->getF3()->error(404, 'File not found: ' . $filePath); + self::getF3()->error(404, 'File not found: ' . $filePath); } } + return $tableData; + } + + /** + * import table data from a *.csv file + * @return array|bool + */ + public function importData(){ + $status = false; + + if( + static::$enableDataImport && + !empty($tableData = self::loadCSV($this->getTable())) + ){ + // import row data + $status = $this->importStaticData($tableData); + $this->getF3()->status(202); + } + return $status; } @@ -896,6 +972,15 @@ abstract class AbstractModel extends Cortex { return \Base::instance(); } + /** + * get model data as array + * @param $data + * @return array + */ + public static function toArray($data) : array { + return json_decode(json_encode($data), true); + } + /** * get new filter array representation * -> $suffix can be used fore unique placeholder, @@ -1019,6 +1104,17 @@ abstract class AbstractModel extends Cortex { return $model; } + /** + * generate hashKey for a complete table + * -> should hold hashKeys for multiple rows + * @param string $table + * @param string $prefix + * @return string + */ + public static function generateHashKeyTable(string $table, string $prefix = self::CACHE_KEY_PREFIX ) : string { + return $prefix . '_' . strtolower($table); + } + /** * overwrites parent * @param null $db diff --git a/app/main/model/pathfinder/alliancemapmodel.php b/app/main/model/pathfinder/alliancemapmodel.php index 2634d514..0152b5ff 100644 --- a/app/main/model/pathfinder/alliancemapmodel.php +++ b/app/main/model/pathfinder/alliancemapmodel.php @@ -12,8 +12,14 @@ use DB\SQL\Schema; class AllianceMapModel extends AbstractPathfinderModel { + /** + * @var string + */ protected $table = 'alliance_map'; + /** + * @var array + */ protected $fieldConf = [ 'active' => [ 'type' => Schema::DT_BOOL, diff --git a/app/main/model/pathfinder/alliancemodel.php b/app/main/model/pathfinder/alliancemodel.php index 1a792f5d..680dcacd 100644 --- a/app/main/model/pathfinder/alliancemodel.php +++ b/app/main/model/pathfinder/alliancemodel.php @@ -13,8 +13,14 @@ use lib\Config; class AllianceModel extends AbstractPathfinderModel { + /** + * @var string + */ protected $table = 'alliance'; + /** + * @var array + */ protected $fieldConf = [ 'active' => [ 'type' => Schema::DT_BOOL, @@ -145,7 +151,7 @@ class AllianceModel extends AbstractPathfinderModel { if($this->isOutdated()){ // request alliance data $allianceData = self::getF3()->ccpClient()->getAllianceData($id); - if( !empty($allianceData) ){ + if(!empty($allianceData) && !isset($allianceData['error'])){ $this->copyfrom($allianceData, ['id', 'name', 'ticker']); $this->save(); } diff --git a/app/main/model/pathfinder/characterlogmodel.php b/app/main/model/pathfinder/characterlogmodel.php index 64ce483f..1e4a6b10 100644 --- a/app/main/model/pathfinder/characterlogmodel.php +++ b/app/main/model/pathfinder/characterlogmodel.php @@ -96,6 +96,15 @@ class CharacterLogModel extends AbstractPathfinderModel { 'default' => '', 'activity-log' => true ], + 'structureTypeId' => [ + 'type' => Schema::DT_INT, + 'index' => true + ], + 'structureTypeName' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], 'structureId' => [ 'type' => Schema::DT_BIGINT, 'index' => true, @@ -146,9 +155,13 @@ class CharacterLogModel extends AbstractPathfinderModel { } if( isset($logData['structure']) ){ + $this->structureTypeId = (int)$logData['structure']['type']['id']; + $this->structureTypeName = $logData['structure']['type']['name']; $this->structureId = (int)$logData['structure']['id']; $this->structureName = $logData['structure']['name']; }else{ + $this->structureTypeId = null; + $this->structureTypeName = ''; $this->structureId = null; $this->structureName = ''; } @@ -178,20 +191,15 @@ class CharacterLogModel extends AbstractPathfinderModel { $logData->station->name = $this->stationName; $logData->structure = (object) []; + $logData->structure->type = (object) []; + $logData->structure->type->id = $this->structureTypeId; + $logData->structure->type->name = $this->structureTypeName; $logData->structure->id = (int)$this->structureId; $logData->structure->name = $this->structureName; return $logData; } - /** - * get 'character log' data as array - * @return array - */ - public function getDataAsArray() : array { - return json_decode(json_encode($this->getData()), true); - } - /** * Event "Hook" function * return false will stop any further action diff --git a/app/main/model/pathfinder/charactermodel.php b/app/main/model/pathfinder/charactermodel.php index 6e99a4cb..1b22bbf1 100644 --- a/app/main/model/pathfinder/charactermodel.php +++ b/app/main/model/pathfinder/charactermodel.php @@ -499,14 +499,7 @@ class CharacterModel extends AbstractPathfinderModel { * @return UserModel|null */ public function getUser() : ?UserModel { - $user = null; - if($this->hasUserCharacter()){ - /** - * @var $user UserModel - */ - $user = $this->userCharacter->userId; - } - return $user; + return $this->hasUserCharacter() ? $this->userCharacter->userId : null; } /** @@ -864,23 +857,22 @@ class CharacterModel extends AbstractPathfinderModel { if($this->isOnline($accessToken)){ $locationData = self::getF3()->ccpClient()->getCharacterLocationData($this->_id, $accessToken); - if( !empty($locationData['system']['id']) ){ + if(!empty($locationData['system']['id'])){ // character is currently in-game - // get current $characterLog or get new --------------------------------------------------- - if( !($characterLog = $this->getLog()) ){ + // 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(); + $logData = $characterLog::toArray($characterLog->getData()); - // check system and station data for changes ---------------------------------------------- + // check system and station data for changes -------------------------------------------------- // IDs for "systemId", "stationId" that require more data $lookupUniverseIds = []; - if( empty($logData['system']['name']) || $logData['system']['id'] !== $locationData['system']['id'] @@ -889,33 +881,18 @@ class CharacterModel extends AbstractPathfinderModel { $lookupUniverseIds[] = $locationData['system']['id']; } - if( !empty($locationData['station']['id']) ){ - if( - empty($logData['station']['name']) || - $logData['station']['id'] !== $locationData['station']['id'] - ){ - // station changed -> request "station name" for current station - $lookupUniverseIds[] = $locationData['station']['id']; - } - }else{ - unset($logData['station']); - } - $logData = array_replace_recursive($logData, $locationData); - // get "more" data for systemId and/or stationId ----------------------------------------- - if( !empty($lookupUniverseIds) ){ + // get "more" data for systemId --------------------------------------------------------------- + if(!empty($lookupUniverseIds)){ // get "more" information for some Ids (e.g. name) $universeData = self::getF3()->ccpClient()->getUniverseNamesData($lookupUniverseIds); - if( !empty($universeData) && !isset($universeData['error']) ){ + 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{ @@ -924,32 +901,63 @@ class CharacterModel extends AbstractPathfinderModel { } } - // check structure data for changes ------------------------------------------------------- + // check station data for changes ------------------------------------------------------------- if(!$deleteLog){ + // IDs for "stationId" that require more data + $lookupStationId = 0; + if(!empty($locationData['station']['id'])){ + if( + empty($logData['station']['name']) || + $logData['station']['id'] !== $locationData['station']['id'] + ){ + // station changed -> request station data + $lookupStationId = $locationData['station']['id']; + } + }else{ + unset($logData['station']); + } + // get "more" data for stationId + if($lookupStationId > 0){ + /** + * @var $stationModel Universe\StationModel + */ + $stationModel = Universe\AbstractUniverseModel::getNew('StationModel'); + $stationModel->loadById($lookupStationId, $accessToken, $additionalOptions); + if($stationModel->valid()){ + $stationData['station'] = $stationModel::toArray($stationModel->getData()); + $logData = array_replace_recursive($logData, $stationData); + }else{ + unset($logData['station']); + } + } + } + + // check structure data for changes ----------------------------------------------------------- + if(!$deleteLog){ // IDs for "structureId" that require more data $lookupStructureId = 0; - if( !empty($locationData['structure']['id']) ){ + if(!empty($locationData['structure']['id'])){ if( empty($logData['structure']['name']) || $logData['structure']['id'] !== $locationData['structure']['id'] ){ - // structure changed -> request "structure name" for current station + // structure changed -> request structure data $lookupStructureId = $locationData['structure']['id']; } }else{ unset($logData['structure']); } - // get "more" data for structureId --------------------------------------------------- + // 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(); + if($structureModel->valid()){ + $structureData['structure'] = $structureModel::toArray($structureModel->getData()); $logData = array_replace_recursive($logData, $structureData); }else{ unset($logData['structure']); @@ -957,13 +965,13 @@ class CharacterModel extends AbstractPathfinderModel { } } - // check ship data for changes ------------------------------------------------------------ - if( !$deleteLog ){ + // 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($shipData['ship']['typeId'])){ if( empty($logData['ship']['typeName']) || $logData['ship']['typeId'] !== $shipData['ship']['typeId'] @@ -980,7 +988,7 @@ class CharacterModel extends AbstractPathfinderModel { $invalidResponse = true; } - // get "more" data for shipTypeId ---------------------------------------------------- + // get "more" data for shipTypeId if($lookupShipTypeId > 0){ /** * @var $typeModel Universe\TypeModel @@ -997,7 +1005,7 @@ class CharacterModel extends AbstractPathfinderModel { } } - if( !$deleteLog ){ + if(!$deleteLog){ // mark log as "updated" even if no changes were made if($additionalOptions['markUpdated'] === true){ $characterLog->touch('updated'); @@ -1086,7 +1094,7 @@ class CharacterModel extends AbstractPathfinderModel { ){ $task = 'add'; $mapIds = []; - $historyLog = $characterLog->getDataAsArray(); + $historyLog = $characterLog::toArray($characterLog->getData()); if($logHistoryData = $this->getLogsHistory()){ // skip logging if no relevant fields changed diff --git a/app/main/model/pathfinder/connectionmodel.php b/app/main/model/pathfinder/connectionmodel.php index 6f361c6d..9eb3139e 100644 --- a/app/main/model/pathfinder/connectionmodel.php +++ b/app/main/model/pathfinder/connectionmodel.php @@ -14,8 +14,14 @@ use lib\logging; class ConnectionModel extends AbstractMapTrackingModel { + /** + * @var string + */ protected $table = 'connection'; + /** + * @var array + */ protected $fieldConf = [ 'active' => [ 'type' => Schema::DT_BOOL, diff --git a/app/main/model/pathfinder/corporationmapmodel.php b/app/main/model/pathfinder/corporationmapmodel.php index 5cdcc571..8fe73e1c 100644 --- a/app/main/model/pathfinder/corporationmapmodel.php +++ b/app/main/model/pathfinder/corporationmapmodel.php @@ -12,8 +12,14 @@ use DB\SQL\Schema; class CorporationMapModel extends AbstractPathfinderModel { + /** + * @var string + */ protected $table = 'corporation_map'; + /** + * @var array + */ protected $fieldConf = [ 'active' => [ 'type' => Schema::DT_BOOL, diff --git a/app/main/model/pathfinder/corporationmodel.php b/app/main/model/pathfinder/corporationmodel.php index d464411d..b2a5cb71 100644 --- a/app/main/model/pathfinder/corporationmodel.php +++ b/app/main/model/pathfinder/corporationmodel.php @@ -13,6 +13,9 @@ use lib\Config; class CorporationModel extends AbstractPathfinderModel { + /** + * @var string + */ protected $table = 'corporation'; /** @@ -91,6 +94,9 @@ class CorporationModel extends AbstractPathfinderModel { 'map_export' ]; + /** + * @var array + */ protected $fieldConf = [ 'active' => [ 'type' => Schema::DT_BOOL, @@ -181,17 +187,19 @@ class CorporationModel extends AbstractPathfinderModel { /** * get all maps for this corporation - * @param array $mapIds + * @param int|null $mapId * @param array $options * @return array */ - public function getMaps($mapIds = [], $options = []) : array { + public function getMaps(?int $mapId = null, $options = []) : array { $maps = []; $this->filterRel(); - if(!empty($mapIds)){ - $filters = []; - $filters[] = ['mapId IN (:mapId)', ':mapId' => $mapIds]; + if($mapId){ + $filters = [ + self::getFilter('mapId', $mapId) + ]; + $this->filter('mapCorporations', $this->mergeWithRelFilter('mapCorporations', $this->mergeFilter($filters)), $this->getRelFilterOption('mapCorporations')); } @@ -217,7 +225,7 @@ class CorporationModel extends AbstractPathfinderModel { * @param array $options * @return CharacterModel[] */ - public function getCharacters($characterIds = [], $options = []){ + public function getCharacters($characterIds = [], $options = []) : array { $characters = []; $filter = ['active = ?', 1]; @@ -244,30 +252,28 @@ class CorporationModel extends AbstractPathfinderModel { /** * get all structure data for this corporation - * @param array $systemIds + * @param int $systemId * @return array */ - public function getStructuresData(array $systemIds = []) : array { + public function getStructuresData(int $systemId) : array { $structuresData = []; + $structure = $this->rel('structures'); - $this->filter('corporationStructures', ['active = ?', 1]); - $this->has('corporationStructures.structureId', ['active = ?', 1]); + $filters = [ + self::getFilter('corporationId', $this->id), + self::getFilter('active', true) + ]; - if($systemIds){ - if(count($systemIds) == 1){ - $filterSystems = 'systemId = ?'; - $filterSystemIds = reset($systemIds); - }else{ - $filterSystems = 'systemId IN (?)'; - $filterSystemIds = $systemIds; - } + $structure->has('structureCorporations', $this->mergeFilter($filters)); - $this->has('corporationStructures.structureId', [$filterSystems, $filterSystemIds]); - } + $filters = [ + self::getFilter('systemId', $systemId), + self::getFilter('active', true) + ]; - if($this->corporationStructures) { - foreach($this->corporationStructures as $corporationStructure){ - $structuresData[] = $corporationStructure->structureId->getData(); + if($structures = $structure->find($this->mergeFilter($filters))){ + foreach($structures as $structure){ + $structuresData[] = $structure->getData(); } } @@ -351,7 +357,7 @@ class CorporationModel extends AbstractPathfinderModel { if($this->isOutdated()){ // request corporation data $corporationData = self::getF3()->ccpClient()->getCorporationData($id); - if( !empty($corporationData) ){ + if(!empty($corporationData) && !isset($corporationData['error'])){ // check for NPC corporation $corporationData['isNPC'] = self::getF3()->ccpClient()->isNpcCorporation($id); diff --git a/app/main/model/pathfinder/corporationstructuremodel.php b/app/main/model/pathfinder/corporationstructuremodel.php index 5dcfe47b..a9aab364 100644 --- a/app/main/model/pathfinder/corporationstructuremodel.php +++ b/app/main/model/pathfinder/corporationstructuremodel.php @@ -12,8 +12,14 @@ use DB\SQL\Schema; class CorporationStructureModel extends AbstractPathfinderModel { + /** + * @var string + */ protected $table = 'corporation_structure'; + /** + * @var array + */ protected $fieldConf = [ 'active' => [ 'type' => Schema::DT_BOOL, diff --git a/app/main/model/pathfinder/mapmodel.php b/app/main/model/pathfinder/mapmodel.php index e40ce578..2be5ad10 100644 --- a/app/main/model/pathfinder/mapmodel.php +++ b/app/main/model/pathfinder/mapmodel.php @@ -17,6 +17,9 @@ use lib\logging; class MapModel extends AbstractMapTrackingModel { + /** + * @var string + */ protected $table = 'map'; /** @@ -27,6 +30,9 @@ class MapModel extends AbstractMapTrackingModel { const ERROR_SLACK_CHANNEL = 'Invalid #Slack channel column [%s]'; const ERROR_DISCORD_CHANNEL = 'Invalid #Discord channel column [%s]'; + /** + * @var array + */ protected $fieldConf = [ 'active' => [ 'type' => Schema::DT_BOOL, @@ -98,6 +104,12 @@ class MapModel extends AbstractMapTrackingModel { 'default' => 1, 'activity-log' => true ], + 'trackAbyssalJumps' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'activity-log' => true + ], 'logActivity' => [ 'type' => Schema::DT_BOOL, 'nullable' => false, @@ -223,6 +235,7 @@ class MapModel extends AbstractMapTrackingModel { $mapData->deleteEolConnections = $this->deleteEolConnections; $mapData->persistentAliases = $this->persistentAliases; $mapData->persistentSignatures = $this->persistentSignatures; + $mapData->trackAbyssalJumps = $this->trackAbyssalJumps; // map scope $mapData->scope = (object) []; @@ -563,41 +576,37 @@ class MapModel extends AbstractMapTrackingModel { } /** - * get either all system models in this map - * @return SystemModel[] + * get systems in this map + * @return CortexCollection|array */ protected function getSystems(){ - $systems = []; + $filters = [ + self::getFilter('active', true) + ]; - // orderBy x-Coordinate for smoother frontend animation (left to right) - $this->filter('systems', ['active = 1'], - ['order' => 'posX'] - ); - - if($this->systems){ - $systems = $this->systems; - } - - return $systems; + return $this->relFind('systems', $this->mergeFilter($filters)) ? : []; } /** * get all system data for all systems in this map * @return \stdClass[] - * @throws \Exception */ - public function getSystemsData() : array{ - $systemData = []; - $systems = $this->getSystems(); + public function getSystemsData() : array { + $systemsData = []; - foreach($systems as $system){ + foreach($this->getSystems() as $system){ /** * @var $system SystemModel */ - $systemData[] = $system->getData(); + $systemsData[] = $system->getData(); } - return $systemData; + // orderBy x-Coordinate for smoother frontend animation (left to right) + usort($systemsData, function($sysDataA, $sysDataB){ + return $sysDataA->position->x <=> $sysDataB->position->x; + }); + + return $systemsData; } /** @@ -650,25 +659,24 @@ class MapModel extends AbstractMapTrackingModel { * @return \stdClass[] */ public function getConnectionsData() : array { - $connectionData = []; - $connections = $this->getConnections(); + $connectionsData = []; - foreach($connections as $connection){ + foreach($this->getConnections() as $connection){ /** * @var $connection ConnectionModel */ - $connectionData[] = $connection->getData(true); + $connectionsData[] = $connection->getData(true); } - return $connectionData; + return $connectionsData; } /** * get all structures data for this map - * @param array $systemIds + * @param int $systemId * @return array */ - public function getStructuresData(array $systemIds = []) : array { + public function getStructuresData(int $systemId) : array { $structuresData = []; $corporations = $this->getAllCorporations(); @@ -676,7 +684,7 @@ class MapModel extends AbstractMapTrackingModel { // corporations should be unique if( !isset($structuresData[$corporation->_id]) ){ // get all structures for current corporation - $corporationStructuresData = $corporation->getStructuresData($systemIds); + $corporationStructuresData = $corporation->getStructuresData($systemId); if( !empty($corporationStructuresData) ){ // corporation has structures $structuresData[$corporation->_id] = [ @@ -819,11 +827,6 @@ class MapModel extends AbstractMapTrackingModel { $characters = []; $filter = ['active = ?', 1]; - if( !empty($characterIds) ){ - $filter[0] .= ' AND id IN (?)'; - $filter[] = $characterIds; - } - $this->filter('mapCharacters', $filter); if($this->mapCharacters){ @@ -839,7 +842,7 @@ class MapModel extends AbstractMapTrackingModel { * get corporations that have access to this map * @return CorporationModel[] */ - public function getCorporations() : array { + private function getCorporations() : array { $corporations = []; if($this->isCorporation()){ @@ -847,7 +850,7 @@ class MapModel extends AbstractMapTrackingModel { if($this->mapCorporations){ foreach($this->mapCorporations as $mapCorporation){ - $corporations[] = $mapCorporation->corporationId; + $corporations[$mapCorporation->corporationId->_id] = $mapCorporation->corporationId; } } } diff --git a/app/main/model/pathfinder/rightmodel.php b/app/main/model/pathfinder/rightmodel.php index 71dd0fa3..2c13b877 100644 --- a/app/main/model/pathfinder/rightmodel.php +++ b/app/main/model/pathfinder/rightmodel.php @@ -12,8 +12,14 @@ use DB\SQL\Schema; class RightModel extends AbstractPathfinderModel { + /** + * @var string + */ protected $table = 'right'; + /** + * @var array + */ protected $fieldConf = [ 'active' => [ 'type' => Schema::DT_BOOL, @@ -24,7 +30,9 @@ class RightModel extends AbstractPathfinderModel { 'name' => [ 'type' => Schema::DT_VARCHAR128, 'nullable' => false, - 'default' => '' + 'default' => '', + 'index' => true, + 'unique' => true ], 'label' => [ 'type' => Schema::DT_VARCHAR128, @@ -41,6 +49,9 @@ class RightModel extends AbstractPathfinderModel { ] ]; + /** + * @var array + */ protected static $tableData = [ [ 'id' => 1, diff --git a/app/main/model/pathfinder/structuremodel.php b/app/main/model/pathfinder/structuremodel.php index 3f3b64d4..f4b8de7c 100644 --- a/app/main/model/pathfinder/structuremodel.php +++ b/app/main/model/pathfinder/structuremodel.php @@ -19,11 +19,6 @@ class StructureModel extends AbstractPathfinderModel { */ protected $table = 'structure'; - /** - * categoryId (from ESI) that holds all "groups" with structure "types" - */ - const CATEGORY_STRUCTURE_ID = 65; - /** * @var array */ @@ -130,15 +125,15 @@ class StructureModel extends AbstractPathfinderModel { /** * set corporationId (owner) for this structure * -> if corporation does not exists in DB -> load from API - * @param int $corporationId + * @param int|null $corporationId * @return int|null */ - public function set_corporationId(int $corporationId) : ?int { + public function set_corporationId(?int $corporationId) : ?int { $oldCorporationId = $this->get('corporationId', true) ? : 0; if($corporationId){ if($corporationId !== $oldCorporationId){ - // make sure there is already corporation data stored for new corporationId + // make sure there is already corporation data available for new corporationId /** * @var CorporationModel $corporation */ @@ -156,26 +151,20 @@ class StructureModel extends AbstractPathfinderModel { } /** * validates systemId + * -> a structure always belongs to the same system * @param string $key * @param string $val * @return bool */ - protected function validate_systemId(string $key, string $val): bool { - $valid = true; - - if( !$this->dry() && $this->systemId !== (int)$val ){ - // structure always belongs to the same system - $valid = false; - } - - return $valid; + protected function validate_systemId(string $key, string $val) : bool { + return !($this->valid() && $this->systemId !== (int)$val); } /** * check whether this model is valid or not * @return bool */ - public function isValid(): bool { + public function isValid() : bool { if($valid = parent::isValid()){ // structure always belongs to a systemId if(!(int)$this->systemId){ @@ -244,7 +233,7 @@ class StructureModel extends AbstractPathfinderModel { * @param string $name * @param int $systemId */ - public function getByName(CorporationModel $corporation, string $name, int $systemId) { + public function getByName(CorporationModel $corporation, string $name, int $systemId){ if( !$corporation->dry() && $name){ $this->has('structureCorporations', ['corporationId = :corporationId', ':corporationId' => $corporation->_id]); $this->load(['name = :name AND systemId = :systemId AND active = :active', diff --git a/app/main/model/pathfinder/structurestatusmodel.php b/app/main/model/pathfinder/structurestatusmodel.php index bb37d6f1..5d7aca0c 100644 --- a/app/main/model/pathfinder/structurestatusmodel.php +++ b/app/main/model/pathfinder/structurestatusmodel.php @@ -13,8 +13,14 @@ use DB\SQL\Schema; class StructureStatusModel extends AbstractPathfinderModel { + /** + * @var string + */ protected $table = 'structure_status'; + /** + * @var array + */ protected $fieldConf = [ 'active' => [ 'type' => Schema::DT_BOOL, @@ -42,6 +48,9 @@ class StructureStatusModel extends AbstractPathfinderModel { ] ]; + /** + * @var array + */ protected static $tableData = [ [ 'id' => 1, diff --git a/app/main/model/pathfinder/systemmodel.php b/app/main/model/pathfinder/systemmodel.php index 8084ea31..29ec2de9 100644 --- a/app/main/model/pathfinder/systemmodel.php +++ b/app/main/model/pathfinder/systemmodel.php @@ -10,6 +10,7 @@ namespace Model\Pathfinder; use DB\SQL\Schema; use lib\logging; +use lib\PriorityCacheStore; use Controller\Ccp\Universe; class SystemModel extends AbstractMapTrackingModel { @@ -39,16 +40,16 @@ class SystemModel extends AbstractMapTrackingModel { */ const DATA_CACHE_KEY_SIGNATURES_HISTORY = 'HISTORY_SIGNATURES'; + /** + * @var PriorityCacheStore + */ + protected static $priorityCacheStore; + /** * @var string */ protected $table = 'system'; - /** - * @var array - */ - protected $staticSystemDataCache = []; - /** * @var array */ @@ -161,80 +162,79 @@ class SystemModel extends AbstractMapTrackingModel { /** * get map data as object * @return \stdClass - * @throws \Exception */ public function getData(){ - // check if there is cached data - $systemData = $this->getCacheData(); - - if(is_null($systemData)){ - // no cached system data found - - $systemData = (object) []; - $systemData->id = $this->_id; - $systemData->mapId = is_object($this->mapId) ? $this->get('mapId', true) : 0; - $systemData->systemId = $this->systemId; - $systemData->alias = $this->alias; + if(is_null($data = $this->getCacheData())){ + $data = (object) []; + $data->id = $this->_id; + $data->mapId = is_object($this->mapId) ? $this->get('mapId', true) : 0; + $data->systemId = $this->systemId; + $data->alias = $this->alias; if(is_object($this->typeId)){ - $systemData->type = $this->typeId->getData(); + $data->type = $this->typeId->getData(); } if(is_object($this->statusId)){ - $systemData->status = $this->statusId->getData(); + $data->status = $this->statusId->getData(); } - $systemData->locked = $this->locked; - $systemData->rallyUpdated = strtotime($this->rallyUpdated); - $systemData->rallyPoke = $this->rallyPoke; - $systemData->description = $this->description ? : ''; + $data->locked = $this->locked; + $data->drifter = $this->isDrifter(); + $data->rallyUpdated = strtotime($this->rallyUpdated); + $data->rallyPoke = $this->rallyPoke; + $data->description = $this->description ? : ''; - $systemData->position = (object) []; - $systemData->position->x = $this->posX; - $systemData->position->y = $this->posY; + $data->position = (object) []; + $data->position->x = $this->posX; + $data->position->y = $this->posY; - $systemData->created = (object) []; - $systemData->created->created = strtotime($this->created); + $data->created = (object) []; + $data->created->created = strtotime($this->created); if(is_object($this->createdCharacterId)){ - $systemData->created->character = $this->createdCharacterId->getData(); + $data->created->character = $this->createdCharacterId->getData(); } - $systemData->updated = (object) []; - $systemData->updated->updated = strtotime($this->updated); + $data->updated = (object) []; + $data->updated->updated = strtotime($this->updated); if(is_object($this->updatedCharacterId)){ - $systemData->updated->character = $this->updatedCharacterId->getData(); + $data->updated->character = $this->updatedCharacterId->getData(); } // static system data ------------------------------------------------------------------------------------- - $systemData->name = $this->name; - $systemData->security = $this->security; - $systemData->trueSec = $this->trueSec; - $systemData->effect = $this->effect; - $systemData->shattered = $this->shattered; + $data->name = $this->name; + $data->security = $this->security; + $data->trueSec = $this->trueSec; + $data->effect = $this->effect; + $data->shattered = $this->shattered; - $systemData->constellation = (object) []; - $systemData->constellation->id = $this->constellationId; - $systemData->constellation->name = $this->constellation; + $data->constellation = (object) []; + $data->constellation->id = $this->constellationId; + $data->constellation->name = $this->constellation; - $systemData->region = (object) []; - $systemData->region->id = $this->regionId; - $systemData->region->name = $this->region; + $data->region = (object) []; + $data->region->id = $this->regionId; + $data->region->name = $this->region; - $systemData->planets = $this->planets ? : []; - $systemData->statics = $this->statics ? : []; + $data->planets = $this->planets ? : []; + $data->statics = $this->statics ? : []; - if(is_object($this->faction)){ - $systemData->faction = $this->faction; + if(is_object($sovereignty = $this->sovereignty)){ + $data->sovereignty = $sovereignty; + } + + if(is_object($factionWar = $this->factionWar)){ + $data->factionWar = $factionWar; } // 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($systemData); + $this->updateCacheData($data); } - return $systemData; + return $data; } /** @@ -244,14 +244,19 @@ class SystemModel extends AbstractMapTrackingModel { */ private function getStaticSystemData(){ $staticData = null; - if( !empty($this->staticSystemDataCache[$this->systemId]) ){ - $staticData = $this->staticSystemDataCache[$this->systemId]; + if(!is_object(self::$priorityCacheStore)){ + self::$priorityCacheStore = new PriorityCacheStore(); + } + + if(self::$priorityCacheStore->exists($this->systemId)){ + $staticData = self::$priorityCacheStore->get($this->systemId); }else{ $staticData = (new Universe())->getSystemData($this->systemId); if($staticData){ - $this->staticSystemDataCache = [$this->systemId => $staticData]; + self::$priorityCacheStore->set($this->systemId, $staticData); } } + return $staticData; } @@ -435,10 +440,6 @@ class SystemModel extends AbstractMapTrackingModel { return ($constellationData && $constellationData->region) ? $constellationData->region->name : null; } - public function get_faction(){ - return $this->getStaticSystemValue('faction'); - } - public function get_security(){ return $this->getStaticSystemValue('security'); } @@ -463,6 +464,18 @@ class SystemModel extends AbstractMapTrackingModel { return $this->getStaticSystemValue('planets'); } + public function get_stations(){ + return $this->getStaticSystemValue('stations'); + } + + public function get_sovereignty(){ + return $this->getStaticSystemValue('sovereignty'); + } + + public function get_factionWar(){ + return $this->getStaticSystemValue('factionWar'); + } + /** * Event "Hook" function * @param self $self @@ -652,7 +665,15 @@ class SystemModel extends AbstractMapTrackingModel { * @return \stdClass[] */ public function getStructuresData() : array { - return $this->getMap()->getStructuresData([$this->systemId]); + return $this->getMap()->getStructuresData($this->systemId); + } + + /** + * get data for all stations in this system + * @return array + */ + public function getStationsData() : array { + return $this->stations ? : []; } /** @@ -679,6 +700,14 @@ class SystemModel extends AbstractMapTrackingModel { return ($this->typeId->id === 3 && $this->security === 'A'); } + /** + * check whether this system is in drifter-space + * @return bool + */ + public function isDrifter() : bool { + return in_array($this->security, ['C14', 'C15', 'C16', 'C17', 'C18']); + } + /** * send rally point poke to various "APIs" * -> send to a Slack channel diff --git a/app/main/model/pathfinder/systemneighbourmodel.php b/app/main/model/pathfinder/systemneighbourmodel.php deleted file mode 100644 index 289ded4f..00000000 --- a/app/main/model/pathfinder/systemneighbourmodel.php +++ /dev/null @@ -1,49 +0,0 @@ - [ - 'type' => Schema::DT_INT, - 'index' => true - ], - 'constellationId' => [ - 'type' => Schema::DT_INT, - 'index' => true - ], - 'systemName' => [ - 'type' => Schema::DT_VARCHAR128, - 'default' => '' - ], - 'systemId' => [ - 'type' => Schema::DT_INT, - 'index' => true - ], - 'jumpNodes' => [ - 'type' => Schema::DT_VARCHAR512, - 'default' => '' - ], - 'trueSec' => [ - 'type' => Schema::DT_DECIMAL, - 'default' => 0 - ] - ]; - - /** - * No static columns added - * @var bool - */ - protected $addStaticFields = false; -} \ No newline at end of file diff --git a/app/main/model/pathfinder/systemsignaturemodel.php b/app/main/model/pathfinder/systemsignaturemodel.php index 1c0f9d60..94ed95d6 100644 --- a/app/main/model/pathfinder/systemsignaturemodel.php +++ b/app/main/model/pathfinder/systemsignaturemodel.php @@ -13,8 +13,14 @@ use lib\logging; class SystemSignatureModel extends AbstractMapTrackingModel { + /** + * @var string + */ protected $table = 'system_signature'; + /** + * @var array + */ protected $fieldConf = [ 'active' => [ 'type' => Schema::DT_BOOL, @@ -278,6 +284,13 @@ class SystemSignatureModel extends AbstractMapTrackingModel { */ public function afterEraseEvent($self, $pkeys){ $self->logActivity('signatureDelete'); + + if( + $self->connectionIdDeleteCascade === true && + ($connection = $self->getConnection()) + ){ + $connection->erase(); + } } /** diff --git a/app/main/model/pathfinder/systemtypemodel.php b/app/main/model/pathfinder/systemtypemodel.php index 87e0c5c7..b4c18db5 100644 --- a/app/main/model/pathfinder/systemtypemodel.php +++ b/app/main/model/pathfinder/systemtypemodel.php @@ -12,8 +12,14 @@ use DB\SQL\Schema; class SystemTypeModel extends AbstractPathfinderModel { + /** + * @var string + */ protected $table = 'system_type'; + /** + * @var array + */ protected $fieldConf = [ 'active' => [ 'type' => Schema::DT_BOOL, @@ -28,6 +34,9 @@ class SystemTypeModel extends AbstractPathfinderModel { ] ]; + /** + * @var array + */ protected static $tableData = [ [ 'id' => 1, diff --git a/app/main/model/pathfinder/usermodel.php b/app/main/model/pathfinder/usermodel.php index f067fe1d..6a01c6fa 100644 --- a/app/main/model/pathfinder/usermodel.php +++ b/app/main/model/pathfinder/usermodel.php @@ -202,16 +202,15 @@ class UserModel extends AbstractPathfinderModel { } /** - * get current character data from session + * get current character from session data * -> if $characterId == 0 -> get first character data (random) * @param int $characterId - * @param bool $objectCheck - * @return array + * @param int $ttl + * @return CharacterModel|null * @throws Exception */ - public function getSessionCharacterData($characterId = 0, $objectCheck = true) : array { + public function getSessionCharacter(int $characterId = 0, int $ttl = self::DEFAULT_SQL_TTL) : ?CharacterModel { $data = []; - $characterId = (int)$characterId; $currentSessionUser = (array)$this->getF3()->get(User::SESSION_KEY_USER); if($this->_id === $currentSessionUser['ID']){ @@ -228,28 +227,22 @@ class UserModel extends AbstractPathfinderModel { } } - if( - $objectCheck === true && - !empty($data) - ){ + if($characterId = (int)$data['ID']){ // check if character still exists on DB (e.g. was manually removed in the meantime) // -> This should NEVER happen just for security and "local development" /** * @var $character CharacterModel */ $character = AbstractPathfinderModel::getNew('CharacterModel'); - $character->getById((int)$data['ID']); + $character->getById($characterId, $ttl); - if( - $character->dry() || - !$character->hasUserCharacter() - ){ - // character data is invalid! - $data = []; + if($character->valid() && $character->hasUserCharacter()){ + // character data is valid! + return $character; } } - return $data; + return null; } /** @@ -259,10 +252,9 @@ class UserModel extends AbstractPathfinderModel { */ public function findSessionCharacterData(int $characterId) : array { $data = []; - if($characterId){ - $sessionCharacters = (array)$this->getF3()->get(User::SESSION_KEY_CHARACTERS); + if($characterId && $this->getF3()->exists(User::SESSION_KEY_CHARACTERS, $sessionCharacters)){ // search for specific characterData - foreach($sessionCharacters as $characterData){ + foreach((array)$sessionCharacters as $characterData){ if($characterId === (int)$characterData['ID']){ $data = $characterData; break; diff --git a/app/main/model/universe/abstractuniversemodel.php b/app/main/model/universe/abstractuniversemodel.php index 118b379c..f2b85df8 100644 --- a/app/main/model/universe/abstractuniversemodel.php +++ b/app/main/model/universe/abstractuniversemodel.php @@ -20,7 +20,7 @@ abstract class AbstractUniverseModel extends AbstractModel { /** * */ - const CACHE_KEY_PREFIX = 'index_universe_'; + const CACHE_KEY_PREFIX = parent::CACHE_KEY_PREFIX . '_' . self::DB_ALIAS; /** * cache key for model data -> should "never" expire @@ -36,6 +36,21 @@ abstract class AbstractUniverseModel extends AbstractModel { return null; } + /** + * setter for positions array (x/y/z) + * @param $position + * @return null + */ + public function set_position($position){ + $position = (array)$position; + if(count($position) === 3){ + $this->x = $position['x']; + $this->y = $position['y']; + $this->z = $position['z']; + } + return null; + } + /** * Event "Hook" function * return false will stop any further action @@ -46,7 +61,7 @@ abstract class AbstractUniverseModel extends AbstractModel { public function beforeUpdateEvent($self, $pkeys) : bool { // if model changed, 'update' col needs to be updated as well // -> data no longer "outdated" - $this->touch('updated'); + $self->touch('updated'); return parent::beforeUpdateEvent($self, $pkeys); } @@ -59,7 +74,7 @@ abstract class AbstractUniverseModel extends AbstractModel { */ public function getHashKey(string $column = '_id'){ $key = false; - if( !$this->dry() && $this->exists($column) ){ + if($this->valid() && $this->exists($column)){ $key = self::generateHashKeyRow($this->getTable(), $this->$column); } return $key; @@ -110,21 +125,6 @@ abstract class AbstractUniverseModel extends AbstractModel { return $data; } - /** - * add $rowKeys (hashKeys) to a search index that holds all rowKeys of a table - * @param AbstractUniverseModel $model - * @param array $rowKeys - */ - public static function buildTableIndex(AbstractUniverseModel $model, array $rowKeys = []){ - $hashKeyTable = self::generateHashKeyTable($model->getTable()); - if( !self::getF3()->exists($hashKeyTable, $cachedData) ){ - $cachedData = []; - } - $cachedData = array_unique(array_merge($cachedData, $rowKeys)); - - self::getF3()->set($hashKeyTable, $cachedData, self::CACHE_INDEX_EXPIRE_KEY); - } - /** * get data from "search" index for this model * -> if data not found -> try to build up index for this model @@ -166,6 +166,45 @@ abstract class AbstractUniverseModel extends AbstractModel { */ abstract protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []); + /** + * convert CCPs ids for system security into Pathfinder security label + * -> used e.g. in "Dogma Attributes" (wormholeTargetSystemClass) for wormhole types + * @param int $id + * @return string|null + */ + public static function getSystemSecurityFromId(int $id) : ?string { + $security = null; + if( + ($id >= 1 && $id <= 6) || + ($id >= 12 && $id <= 18) + ){ + $security = 'C' . $id; + }elseif($id == 7){ + $security = 'H'; + }elseif($id == 8){ + $security = 'L'; + }elseif($id == 9){ + $security = '0.0'; + } + + return $security; + } + + /** + * add $rowKeys (hashKeys) to a search index that holds all rowKeys of a table + * @param AbstractUniverseModel $model + * @param array $rowKeys + */ + public static function buildTableIndex(AbstractUniverseModel $model, array $rowKeys = []){ + $hashKeyTable = static::generateHashKeyTable($model->getTable()); + if( !self::getF3()->exists($hashKeyTable, $cachedData) ){ + $cachedData = []; + } + $cachedData = array_unique(array_merge($cachedData, $rowKeys)); + + self::getF3()->set($hashKeyTable, $cachedData, self::CACHE_INDEX_EXPIRE_KEY); + } + /** * generate hashKey for a table row data for search index build * @param string $table @@ -173,16 +212,17 @@ abstract class AbstractUniverseModel extends AbstractModel { * @return string */ public static function generateHashKeyRow(string $table, $value) : string { - return self::generateHashKeyTable($table) . '_' . md5(strtolower((string)$value)); + return static::generateHashKeyTable($table) . '_' . md5(strtolower((string)$value)); } /** * generate hashKey for a complete table * -> should hold hashKeys for multiple rows * @param string $table + * @param string $prefix * @return string */ - public static function generateHashKeyTable(string $table) : string { - return self::CACHE_KEY_PREFIX . strtolower($table); + public static function generateHashKeyTable(string $table, string $prefix = self::CACHE_KEY_PREFIX) : string { + return parent::generateHashKeyTable($table, $prefix); } } \ No newline at end of file diff --git a/app/main/model/universe/alliancemodel.php b/app/main/model/universe/alliancemodel.php new file mode 100644 index 00000000..7a1b3f49 --- /dev/null +++ b/app/main/model/universe/alliancemodel.php @@ -0,0 +1,103 @@ + [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'ticker' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'dateFounded' => [ + 'type' => Schema::DT_DATETIME, + 'default' => null + ], + 'factionId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Model\Universe\FactionModel', + 'constraint' => [ + [ + 'table' => 'faction', + 'on-delete' => 'SET NULL' + ] + ] + ], + 'corporations' => [ + 'has-many' => ['Model\Universe\CorporationModel', 'allianceId'] + ], + 'sovereigntySystems' => [ + 'has-many' => ['Model\Universe\SovereigntyMapModel', 'allianceId'] + ] + ]; + + /** + * get data + * @return \stdClass + */ + public function getData(){ + $data = (object) []; + $data->id = $this->_id; + $data->name = $this->name; + $data->ticker = $this->ticker; + + return $data; + } + + /** + * @param $date + * @return string|null + */ + public function set_dateFounded($date){ + if(is_string($date) && !empty($date)){ + try{ + $dateTime = new \DateTime($date); + $date = $dateTime->format('Y-m-d H:i:s'); + }catch(\Exception $e){ + $date = null; + } + } + return $date; + } + + /** + * load alliance by Id either from DB or load data from API + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ + $data = self::getF3()->ccpClient()->getAllianceData($id); + if(!empty($data) && !isset($data['error'])){ + if($data['factionId']){ + /** + * @var $faction FactionModel + */ + $faction = $this->rel('factionId'); + $faction->loadById($data['factionId'], $accessToken, $additionalOptions); + $data['factionId'] = $faction; + } + + $this->copyfrom($data, ['id', 'name', 'ticker', 'dateFounded', 'factionId']); + $this->save(); + } + } +} \ No newline at end of file diff --git a/app/main/model/universe/categorymodel.php b/app/main/model/universe/categorymodel.php index f8ba5052..3d533143 100644 --- a/app/main/model/universe/categorymodel.php +++ b/app/main/model/universe/categorymodel.php @@ -38,7 +38,7 @@ class CategoryModel extends AbstractUniverseModel { */ public function getData(array $additionalData = []){ $categoryData = (object) []; - $categoryData->id = $this->id; + $categoryData->id = $this->_id; $categoryData->name = $this->name; if($groupsData = $this->getGroupsData($additionalData)){ @@ -77,6 +77,9 @@ class CategoryModel extends AbstractUniverseModel { $groupsData = []; $groups = $this->getGroups(); + /** + * @var $group GroupModel + */ foreach($groups as $group){ $groupsData[] = $group->getData($additionalData); } @@ -84,6 +87,15 @@ class CategoryModel extends AbstractUniverseModel { return $groupsData; } + /** + * get groups count + * @param bool $published + * @return int + */ + public function getGroupsCount(bool $published = true) : int { + return $this->valid() ? count($this->getGroups($published)) : 0; + } + /** * count all types that belong to groups in this category * @param bool $published @@ -91,7 +103,7 @@ class CategoryModel extends AbstractUniverseModel { */ public function getTypesCount(bool $published = true) : int { $count = 0; - if( !$this->dry() ){ + if($this->valid()){ /** * @var $group GroupModel */ @@ -109,8 +121,7 @@ class CategoryModel extends AbstractUniverseModel { * @param array $additionalOptions */ protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ - $data = self::getF3()->ccpClient()->getUniverseCategoryData($id); - if(!empty($data)){ + if(!empty($data = self::getUniverseCategoryData($id))){ $this->copyfrom($data, ['id', 'name', 'published']); $this->save(); } @@ -123,25 +134,71 @@ class CategoryModel extends AbstractUniverseModel { * @return array */ public function loadGroupsData(int $offset = 0, int $length = 0) : array { - $groupIds = []; - if( !$this->dry() ){ - $data = self::getF3()->ccpClient()->getUniverseCategoryData($this->_id); - if(!empty($data)){ - array_multisort($data['groups'], SORT_ASC, SORT_NUMERIC); - if($length){ - $data['groups'] = array_slice($data['groups'], $offset, $length); - } - foreach($data['groups'] as $groupId){ - /** - * @var $group GroupModel - */ - $group = $this->rel('groups'); - $group->loadById($groupId); - $groupIds[] = $groupId; - $group->reset(); - } + $info = ['countAll' => 0, 'countChunk' => 0, 'count' => 0, 'offset' => $offset, 'groupTypes' => []]; + + if( + $this->valid() && + !empty($data = self::getUniverseCategoryData($this->_id)) + ){ + $info['countAll'] = count($data['groups']); + + array_multisort($data['groups'], SORT_ASC, SORT_NUMERIC); + if($length){ + $data['groups'] = array_slice($data['groups'], $offset, $length); + } + + $info['countChunk'] = count($data['groups']); + foreach($data['groups'] as $groupId){ + /** + * @var $group GroupModel + */ + $group = $this->rel('groups'); + $group->loadById($groupId); + + $info['groupTypes'][$groupId] = $group->loadTypesData(); + + $group->reset(); + + $info['count']++; + $info['offset']++; } } - return $groupIds; + + return $info; + } + + /** + * @param int $id + * @return array + */ + public static function getUniverseCategoryData(int $id) : array { + return self::getF3()->ccpClient()->getUniverseCategoryData($id); + } + + /** + * @return array + */ + public static function getUniverseCategories() : array { + return self::getF3()->ccpClient()->getUniverseCategories(); + } + + /** + * @param int $id + * @return array + */ + public static function getUniverseCategoryGroups(int $id) : array { + return empty($data = self::getUniverseCategoryData($id)) ? [] : $data['groups']; + } + + /** + * @param int $id + * @return array + */ + public static function getUniverseCategoryTypes(int $id) : array { + $types = []; + foreach($groupIds = self::getUniverseCategoryGroups($id) as $groupId){ + $types[$groupId] = GroupModel::getUniverseGroupTypes($groupId); + } + return $types; } } \ No newline at end of file diff --git a/app/main/model/universe/constellationmodel.php b/app/main/model/universe/constellationmodel.php index 6e78944a..1f061ecf 100644 --- a/app/main/model/universe/constellationmodel.php +++ b/app/main/model/universe/constellationmodel.php @@ -55,6 +55,9 @@ class ConstellationModel extends AbstractUniverseModel { ], 'systems' => [ 'has-many' => ['Model\Universe\SystemModel', 'constellationId'] + ], + 'systemNeighbours' => [ + 'has-many' => ['Model\Universe\SystemNeighbourModel', 'constellationId'] ] ]; @@ -71,21 +74,6 @@ class ConstellationModel extends AbstractUniverseModel { return $constellationData; } - /** - * setter for positions array (x/y/z) - * @param $position - * @return null - */ - public function set_position($position){ - $position = (array)$position; - if(count($position) === 3){ - $this->x = $position['x']; - $this->y = $position['y']; - $this->z = $position['z']; - } - return null; - } - /** * @param int $id * @param string $accessToken diff --git a/app/main/model/universe/corporationmodel.php b/app/main/model/universe/corporationmodel.php new file mode 100644 index 00000000..d672bc13 --- /dev/null +++ b/app/main/model/universe/corporationmodel.php @@ -0,0 +1,135 @@ + [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'ticker' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'dateFounded' => [ + 'type' => Schema::DT_DATETIME, + 'default' => null + ], + 'memberCount' => [ + 'type' => Schema::DT_INT, + 'nullable' => false, + 'default' => 0 + ], + 'isNPC' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 0 + ], + 'factionId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Model\Universe\FactionModel', + 'constraint' => [ + [ + 'table' => 'faction', + 'on-delete' => 'SET NULL' + ] + ] + ], + 'allianceId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Model\Universe\AllianceModel', + 'constraint' => [ + [ + 'table' => 'alliance', + 'on-delete' => 'SET NULL' + ] + ] + ], + 'sovereigntySystems' => [ + 'has-many' => ['Model\Universe\SovereigntyMapModel', 'corporationId'] + ], + 'stations' => [ + 'has-many' => ['Model\Universe\StationModel', 'corporationId'] + ] + ]; + + /** + * get data + * @return \stdClass + */ + public function getData(){ + $data = (object) []; + $data->id = $this->_id; + $data->name = $this->name; + + return $data; + } + + /** + * @param $date + * @return string|null + */ + public function set_dateFounded($date){ + if(is_string($date) && !empty($date)){ + try{ + $dateTime = new \DateTime($date); + $date = $dateTime->format('Y-m-d H:i:s'); + }catch(\Exception $e){ + $date = null; + } + } + return $date; + } + + /** + * load corporation by Id either from DB or load data from API + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ + $data = self::getF3()->ccpClient()->getCorporationData($id); + if(!empty($data) && !isset($data['error'])){ + // check for NPC corporation + $data['isNPC'] = self::getF3()->ccpClient()->isNpcCorporation($id); + + if($data['factionId']){ + /** + * @var $faction FactionModel + */ + $faction = $this->rel('factionId'); + $faction->loadById($data['factionId'], $accessToken, $additionalOptions); + $data['factionId'] = $faction; + } + + if($data['allianceId']){ + /** + * @var $alliance AllianceModel + */ + $alliance = $this->rel('allianceId'); + $alliance->loadById($data['allianceId'], $accessToken, $additionalOptions); + $data['allianceId'] = $alliance; + } + + $this->copyfrom($data, ['id', 'name', 'ticker', 'dateFounded', 'memberCount', 'isNPC', 'factionId', 'allianceId']); + $this->save(); + } + } +} \ No newline at end of file diff --git a/app/main/model/universe/dogmaattributemodel.php b/app/main/model/universe/dogmaattributemodel.php new file mode 100644 index 00000000..7936c2a8 --- /dev/null +++ b/app/main/model/universe/dogmaattributemodel.php @@ -0,0 +1,97 @@ + [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'displayName' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => true, + 'default' => null + ], + 'description' => [ + 'type' => Schema::DT_TEXT + ], + 'published' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => true, + 'default' => null + ], + 'stackable' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => true, + 'default' => null + ], + 'highIsGood' => [ + 'type' => Schema::DT_BOOL, + 'nullable' => true, + 'default' => null + ], + 'defaultValue' => [ + 'type' => Schema::DT_FLOAT, + 'nullable' => false, + 'default' => 0 + ], + 'iconId' => [ + 'type' => Schema::DT_INT, + 'nullable' => true, + 'default' => null + ], + 'unitId' => [ + 'type' => Schema::DT_INT, + 'nullable' => true, + 'default' => null + ], + 'attributeTypes' => [ + 'has-many' => ['Model\Universe\TypeAttributeModel', 'attributeId'] + ] + ]; + + /** + * get data + * @return \stdClass + */ + public function getData(){ + $attributeData = (object) []; + $attributeData->id = $this->_id; + $attributeData->name = $this->name; + $attributeData->description = $this->description; + + return $attributeData; + } + + /** + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ + $data = self::getF3()->ccpClient()->getDogmaAttributeData($id); + if(!empty($data) && !isset($data['error'])){ + $this->copyfrom($data, ['id', 'name', 'displayName', 'description', 'published', 'stackable', 'highIsGood', 'defaultValue', 'iconId', 'unitId']); + $this->save(); + } + } +} \ No newline at end of file diff --git a/app/main/model/universe/factionmodel.php b/app/main/model/universe/factionmodel.php index 11764aab..bc4623ac 100644 --- a/app/main/model/universe/factionmodel.php +++ b/app/main/model/universe/factionmodel.php @@ -46,8 +46,23 @@ class FactionModel extends AbstractUniverseModel { 'nullable' => false, 'default' => 0 ], - 'systems' => [ - 'has-many' => ['Model\Universe\SystemModel', 'factionId'] + 'race' => [ // faction API endpoint dont have "raceId" data, but race API endpoint has + 'has-one' => ['Model\Universe\RaceModel', 'factionId'] + ], + 'alliances' => [ + 'has-many' => ['Model\Universe\AllianceModel', 'factionId'] + ], + 'corporations' => [ + 'has-many' => ['Model\Universe\CorporationModel', 'factionId'] + ], + 'sovereigntySystems' => [ + 'has-many' => ['Model\Universe\SovereigntyMapModel', 'factionId'] + ], + 'factionWarSystemOwners' => [ + 'has-many' => ['Model\Universe\FactionWarSystemModel', 'ownerFactionId'] + ], + 'factionWarSystemOccupiers' => [ + 'has-many' => ['Model\Universe\FactionWarSystemModel', 'occupierFactionId'] ] ]; @@ -70,7 +85,7 @@ class FactionModel extends AbstractUniverseModel { */ protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ $data = self::getF3()->ccpClient()->getUniverseFactionData($id); - if(!empty($data)){ + if(!empty($data) && !isset($data['error'])){ $this->copyfrom($data, ['id', 'name', 'description', 'sizeFactor', 'stationCount', 'stationSystemCount']); $this->save(); } diff --git a/app/main/model/universe/factionwarsystemmodel.php b/app/main/model/universe/factionwarsystemmodel.php new file mode 100644 index 00000000..943cd09b --- /dev/null +++ b/app/main/model/universe/factionwarsystemmodel.php @@ -0,0 +1,120 @@ + [ + 'type' => Schema::DT_INT, + 'index' => true, + 'unique' => true, + 'belongs-to-one' => 'Model\Universe\SystemModel', + 'constraint' => [ + [ + 'table' => 'system', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'ownerFactionId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Model\Universe\FactionModel', + 'constraint' => [ + [ + 'table' => 'faction', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'occupierFactionId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Model\Universe\FactionModel', + 'constraint' => [ + [ + 'table' => 'faction', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'contested' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'victoryPoints' => [ + 'type' => Schema::DT_INT, + 'nullable' => false, + 'default' => 0 + ], + 'victoryPointsThreshold' => [ + 'type' => Schema::DT_INT, + 'nullable' => false, + 'default' => 0 + ] + ]; + + /** + * No static columns added + * @var bool + */ + protected $addStaticFields = false; + + /** + * get data + * @return \stdClass + */ + public function getData(){ + $data = (object) []; + $data->contested = $this->contested; + + if($this->ownerFactionId){ + $data->ownerFaction = $this->ownerFactionId->getData(); + $data->victoryPercentage = $this->getVictoryPercentage(); + + if( + $this->occupierFactionId && + $this->get('occupierFactionId', true) !== $this->get('ownerFactionId', true) + ){ + $data->occupierFaction = $this->occupierFactionId->getData(); + } + } + + return $data; + } + + /** + * calculate victory progress in percent + * @return int + */ + protected function getVictoryPercentage() : int { + $percent = 0; + + if($this->victoryPoints && $this->victoryPointsThreshold){ + $percent = floor((100 / $this->victoryPointsThreshold) * $this->victoryPoints); + } + + return $percent; + } + + /** + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){} +} \ No newline at end of file diff --git a/app/main/model/universe/groupmodel.php b/app/main/model/universe/groupmodel.php index ec712ab9..1bbf5076 100644 --- a/app/main/model/universe/groupmodel.php +++ b/app/main/model/universe/groupmodel.php @@ -12,8 +12,19 @@ use DB\SQL\Schema; class GroupModel extends AbstractUniverseModel { - protected $table = 'group'; + /** + * @var string + */ + protected $table = 'group'; + /** + * @var bool + */ + public $storeDogmaAttributes = TypeModel::DEFAULT_STORE_DOGMA_ATTRIBUTES; + + /** + * @var array + */ protected $fieldConf = [ 'name' => [ 'type' => Schema::DT_VARCHAR128, @@ -50,7 +61,7 @@ class GroupModel extends AbstractUniverseModel { */ public function getData(array $additionalData = []){ $groupData = (object) []; - $groupData->id = $this->id; + $groupData->id = $this->_id; $groupData->name = $this->name; if($typesData = $this->getTypesData($additionalData)){ @@ -65,7 +76,7 @@ class GroupModel extends AbstractUniverseModel { * @param bool $published * @return array|mixed */ - protected function getTypes(bool $published = true){ + public function getTypes(bool $published = true){ $types = []; if($published){ $this->filter('types', [ @@ -102,7 +113,7 @@ class GroupModel extends AbstractUniverseModel { * @return int */ public function getTypesCount(bool $published = true) : int { - return $this->dry() ? 0 : count($this->getTypes($published)); + return $this->valid() ? count($this->getTypes($published)) : 0; } /** @@ -111,8 +122,7 @@ class GroupModel extends AbstractUniverseModel { * @param array $additionalOptions */ protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ - $data = self::getF3()->ccpClient()->getUniverseGroupData($id); - if(!empty($data)){ + if(!empty($data = self::getUniverseGroupData($id))){ /** * @var $category CategoryModel */ @@ -127,24 +137,62 @@ class GroupModel extends AbstractUniverseModel { /** * load types data for this group - * @return int + * @param int $offset + * @param int $length 0 -> all types + * @return array */ - public function loadTypesData(){ - $count = 0; - if( !$this->dry() ){ - $data = self::getF3()->ccpClient()->getUniverseGroupData($this->_id); - if(!empty($data)){ - foreach((array)$data['types'] as $typeId){ - /** - * @var $type TypeModel - */ - $type = $this->rel('types'); - $type->loadById($typeId); - $type->reset(); - $count++; - } + public function loadTypesData(int $offset = 0, int $length = 0) : array { + $info = ['countAll' => 0, 'countChunk' => 0, 'count' => 0, 'offset' => $offset]; + + if( + $this->valid() && + !empty($data = self::getUniverseGroupData($this->_id)) + ){ + $info['countAll'] = count($data['types']); + + array_multisort($data['types'], SORT_ASC, SORT_NUMERIC); + if($length){ + $data['types'] = array_slice($data['types'], $offset, $length); + } + + $info['countChunk'] = count($data['types']); + foreach($data['types'] as $typeId){ + /** + * @var $type TypeModel + */ + $type = $this->rel('types'); + $type->storeDogmaAttributes = $this->storeDogmaAttributes; + $type->loadById($typeId); + $type->reset(); + + $info['count']++; + $info['offset']++; } } - return $count; + + return $info; + } + + /** + * @param int $id + * @return array + */ + public static function getUniverseGroupData(int $id) : array { + return self::getF3()->ccpClient()->getUniverseGroupData($id); + } + + /** + * @return array + */ + public static function getUniverseGroups() : array { + return self::getF3()->ccpClient()->getUniverseGroups(); + } + + /** + * @param int $id + * @return array + */ + public static function getUniverseGroupTypes(int $id) : array { + return empty($data = self::getUniverseGroupData($id)) ? [] : $data['types']; } } \ No newline at end of file diff --git a/app/main/model/universe/planetmodel.php b/app/main/model/universe/planetmodel.php index 99653034..6b7eefb4 100644 --- a/app/main/model/universe/planetmodel.php +++ b/app/main/model/universe/planetmodel.php @@ -66,28 +66,13 @@ class PlanetModel extends AbstractUniverseModel { * @return \stdClass */ public function getData(){ - $planetData = (object) []; - $planetData->name = $this->name; + $data = (object) []; + $data->name = $this->name; - $planetData->type = (object) []; - $planetData->type->name = $this->typeId->name; + $data->type = (object) []; + $data->type->name = $this->typeId->name; - return $planetData; - } - - /** - * setter for positions array (x/y/z) - * @param $position - * @return null - */ - public function set_position($position){ - $position = (array)$position; - if(count($position) === 3){ - $this->x = $position['x']; - $this->y = $position['y']; - $this->z = $position['z']; - } - return null; + return $data; } /** diff --git a/app/main/model/universe/racemodel.php b/app/main/model/universe/racemodel.php new file mode 100644 index 00000000..deea0ed0 --- /dev/null +++ b/app/main/model/universe/racemodel.php @@ -0,0 +1,82 @@ + [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'description' => [ + 'type' => Schema::DT_TEXT + ], + 'factionId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Model\Universe\FactionModel', + 'constraint' => [ + [ + 'table' => 'faction', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'stations' => [ + 'has-many' => ['Model\Universe\StationModel', 'raceId'] + ] + ]; + + /** + * get data + * @return \stdClass + */ + public function getData(){ + $data = (object) []; + $data->id = $this->_id; + $data->name = $this->name; + $data->faction = $this->factionId->getData(); + + return $data; + } + + /** + * load data from API into $this and save $this + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ + $data = self::getF3()->ccpClient()->getUniverseRaceData($id); + if(!empty($data) && !isset($data['error'])){ + /** + * @var $faction FactionModel + */ + $faction = $this->rel('factionId'); + $faction->loadById($data['factionId'], $accessToken, $additionalOptions); + $data['factionId'] = $faction; + + $this->copyfrom($data, ['id', 'name', 'description', 'factionId']); + $this->save(); + } + } +} \ No newline at end of file diff --git a/app/main/model/universe/regionmodel.php b/app/main/model/universe/regionmodel.php index 46b69b40..77b84fc7 100644 --- a/app/main/model/universe/regionmodel.php +++ b/app/main/model/universe/regionmodel.php @@ -32,6 +32,9 @@ class RegionModel extends AbstractUniverseModel { ], 'constellations' => [ 'has-many' => ['Model\Universe\ConstellationModel', 'regionId'] + ], + 'systemNeighbours' => [ + 'has-many' => ['Model\Universe\SystemNeighbourModel', 'regionId'] ] ]; diff --git a/app/main/model/universe/sovereigntymapmodel.php b/app/main/model/universe/sovereigntymapmodel.php new file mode 100644 index 00000000..59b18b2c --- /dev/null +++ b/app/main/model/universe/sovereigntymapmodel.php @@ -0,0 +1,101 @@ + [ + 'type' => Schema::DT_INT, + 'index' => true, + 'unique' => true, + 'belongs-to-one' => 'Model\Universe\SystemModel', + 'constraint' => [ + [ + 'table' => 'system', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'factionId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Model\Universe\FactionModel', + 'constraint' => [ + [ + 'table' => 'faction', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'allianceId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Model\Universe\AllianceModel', + 'constraint' => [ + [ + 'table' => 'alliance', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'corporationId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Model\Universe\CorporationModel', + 'constraint' => [ + [ + 'table' => 'corporation', + 'on-delete' => 'CASCADE' + ] + ] + ] + ]; + + /** + * No static columns added + * @var bool + */ + protected $addStaticFields = false; + + /** + * get data + * @return \stdClass + */ + public function getData(){ + $data = (object) []; + + if($this->factionId){ + $data->faction = $this->factionId->getData(); + }else{ + if($this->allianceId){ + $data->alliance = $this->allianceId->getData(); + } + + if($this->corporationId){ + $data->corporation = $this->corporationId->getData(); + } + } + + return $data; + } + + /** + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){} +} \ No newline at end of file diff --git a/app/main/model/universe/stargatemodel.php b/app/main/model/universe/stargatemodel.php index d50e5eb6..cbc32a58 100644 --- a/app/main/model/universe/stargatemodel.php +++ b/app/main/model/universe/stargatemodel.php @@ -74,7 +74,6 @@ class StargateModel extends AbstractUniverseModel { ]; public function getData(){ - $stargateData = (object) []; $stargateData->id = $this->_id; $stargateData->type = $this->typeId->name; @@ -83,21 +82,6 @@ class StargateModel extends AbstractUniverseModel { return $stargateData; } - /** - * setter for positions array (x/y/z) - * @param $position - * @return null - */ - public function set_position($position){ - $position = (array)$position; - if(count($position) === 3){ - $this->x = $position['x']; - $this->y = $position['y']; - $this->z = $position['z']; - } - return null; - } - /** * @param int $id * @param string $accessToken diff --git a/app/main/model/universe/starmodel.php b/app/main/model/universe/starmodel.php index 88f8f603..94d21876 100644 --- a/app/main/model/universe/starmodel.php +++ b/app/main/model/universe/starmodel.php @@ -12,8 +12,14 @@ use DB\SQL\Schema; class StarModel extends AbstractUniverseModel { + /** + * @var string + */ protected $table = 'star'; + /** + * @var array + */ protected $fieldConf = [ 'name' => [ 'type' => Schema::DT_VARCHAR128, diff --git a/app/main/model/universe/stationmodel.php b/app/main/model/universe/stationmodel.php new file mode 100644 index 00000000..d890ea59 --- /dev/null +++ b/app/main/model/universe/stationmodel.php @@ -0,0 +1,165 @@ + [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'systemId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Model\Universe\SystemModel', + 'constraint' => [ + [ + 'table' => 'system', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'typeId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Model\Universe\TypeModel', + 'constraint' => [ + [ + 'table' => 'type', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'corporationId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Model\Universe\CorporationModel', + 'constraint' => [ + [ + 'table' => 'corporation', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'raceId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Model\Universe\RaceModel', + 'constraint' => [ + [ + 'table' => 'race', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'services' => [ + 'type' => self::DT_JSON + ], + 'x' => [ + 'type' => Schema::DT_BIGINT, + 'nullable' => false, + 'default' => 0 + ], + 'y' => [ + 'type' => Schema::DT_BIGINT, + 'nullable' => false, + 'default' => 0 + ], + 'z' => [ + 'type' => Schema::DT_BIGINT, + 'nullable' => false, + 'default' => 0 + ] + ]; + + /** + * get data + * @return \stdClass + */ + public function getData(){ + $data = (object) []; + $data->id = $this->_id; + $data->name = $this->name; + $data->type = $this->typeId->getData(); + $data->services = $this->services ? : []; + + // according to ESIs Swagger conf, "raceId" AND "corporationId"(= "owner") are optional + // -> I haven´t seen any imported NPC station data where "raceId" OR "corporationId" not exist + if($this->corporationId){ + $data->corporation = $this->corporationId->getData(); + } + + if($this->raceId){ + $data->race = $this->raceId->getData(); + } + + return $data; + } + + /** + * load data from API into $this and save $this + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ + $data = self::getF3()->ccpClient()->getUniverseStationData($id); + if(!empty($data) && !isset($data['error'])){ + /** + * @var $system SystemModel + */ + $system = $this->rel('systemId'); + $system->loadById($data['systemId'], $accessToken, $additionalOptions); + $data['systemId'] = $system; + + /** + * @var $type TypeModel + */ + $type = $this->rel('typeId'); + $type->loadById($data['typeId'], $accessToken, $additionalOptions); + $data['typeId'] = $type; + + if($data['corporationId']){ + /** + * @var $faction CorporationModel + */ + $corporation = $this->rel('corporationId'); + $corporation->loadById($data['corporationId'], $accessToken, $additionalOptions); + $data['corporationId'] = $corporation; + } + + if($data['raceId']){ + /** + * @var $race RaceModel + */ + $race = $this->rel('raceId'); + $race->loadById($data['raceId'], $accessToken, $additionalOptions); + $data['raceId'] = $race; + } + + $this->copyfrom($data, ['id', 'name', 'systemId', 'typeId', 'corporationId', 'raceId', 'services', 'position']); + $this->save(); + } + } + +} \ No newline at end of file diff --git a/app/main/model/universe/structuremodel.php b/app/main/model/universe/structuremodel.php index 3daee092..a1fc0ae4 100644 --- a/app/main/model/universe/structuremodel.php +++ b/app/main/model/universe/structuremodel.php @@ -13,8 +13,14 @@ use DB\SQL\Schema; class StructureModel extends AbstractUniverseModel { + /** + * @var string + */ protected $table = 'structure'; + /** + * @var array + */ protected $fieldConf = [ 'name' => [ 'type' => Schema::DT_VARCHAR128, @@ -23,9 +29,15 @@ class StructureModel extends AbstractUniverseModel { ], 'systemId' => [ 'type' => Schema::DT_INT, - 'nullable' => false, - 'default' => 0, - 'index' => true + 'index' => true, + 'belongs-to-one' => 'Model\Universe\SystemModel', + 'constraint' => [ + [ + 'table' => 'system', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' ], 'typeId' => [ 'type' => Schema::DT_INT, @@ -63,9 +75,10 @@ class StructureModel extends AbstractUniverseModel { */ public function getData(): \stdClass { $data = (object) []; - if(!$this->dry()){ - $data->id = $this->_id; + if($this->valid()){ + $data->id = $this->_id; $data->name = $this->name; + $data->type = $this->typeId->getData(); } return $data; } @@ -78,7 +91,7 @@ class StructureModel extends AbstractUniverseModel { */ protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ $data = self::getF3()->ccpClient()->getUniverseStructureData($id, $accessToken); - if(!empty($data)){ + if(!empty($data) && !isset($data['error'])){ /** * @var $type TypeModel */ diff --git a/app/main/model/universe/systemmodel.php b/app/main/model/universe/systemmodel.php index b56152d6..39f41672 100644 --- a/app/main/model/universe/systemmodel.php +++ b/app/main/model/universe/systemmodel.php @@ -17,11 +17,6 @@ class SystemModel extends AbstractUniverseModel { */ protected $table = 'system'; - /** - * - */ - const ERROR_INVALID_WORMHOLE = 'Invalid wormhole name "%s" for system: "%s"'; - /** * @var array */ @@ -55,18 +50,6 @@ class SystemModel extends AbstractUniverseModel { ], 'validate' => 'notDry' ], - 'factionId' => [ - 'type' => Schema::DT_INT, - 'index' => true, - 'belongs-to-one' => 'Model\Universe\FactionModel', - 'constraint' => [ - [ - 'table' => 'faction', - 'on-delete' => 'CASCADE' - ] - ], - 'validate' => 'notDry' - ], 'security' => [ 'type' => Schema::DT_VARCHAR128 ], @@ -86,11 +69,6 @@ class SystemModel extends AbstractUniverseModel { 'effect' => [ 'type' => Schema::DT_VARCHAR128 ], - 'shattered' => [ - 'type' => Schema::DT_BOOL, - 'nullable' => false, - 'default' => 0 - ], 'x' => [ 'type' => Schema::DT_BIGINT, 'nullable' => false, @@ -114,6 +92,21 @@ class SystemModel extends AbstractUniverseModel { ], 'stargates' => [ 'has-many' => ['Model\Universe\StargateModel', 'systemId'] + ], + 'stations' => [ + 'has-many' => ['Model\Universe\StationModel', 'systemId'] + ], + 'structures' => [ + 'has-many' => ['Model\Universe\StructureModel', 'systemId'] + ], + 'neighbour' => [ + 'has-one' => ['Model\Universe\SystemNeighbourModel', 'systemId'] + ], + 'sovereignty' => [ + 'has-one' => ['Model\Universe\SovereigntyMapModel', 'systemId'] + ], + 'factionWar' => [ + 'has-one' => ['Model\Universe\FactionWarSystemModel', 'systemId'] ] ]; @@ -123,37 +116,52 @@ class SystemModel extends AbstractUniverseModel { * @return \stdClass */ public function getData(){ - - $systemData = (object) []; - $systemData->id = $this->_id; - $systemData->name = $this->name; - $systemData->constellation = $this->constellationId->getData(); - $systemData->security = $this->security; - $systemData->trueSec = (float)$this->trueSec; - $systemData->effect = $this->effect; - $systemData->shattered = (bool)$this->shattered; + $data = (object) []; + $data->id = $this->_id; + $data->name = $this->name; + $data->constellation = $this->constellationId->getData(); + $data->security = $this->security; + $data->trueSec = (float)$this->trueSec; + $data->effect = $this->effect; + $data->shattered = false; if($this->starId){ - $systemData->star = $this->starId->getData(); + $data->star = $this->starId->getData(); } - if($this->factionId){ - $systemData->faction = $this->factionId->getData(); + if($this->sovereignty){ + $data->sovereignty = $this->sovereignty->getData(); + } + + if($this->factionWar){ + $data->factionWar = $this->factionWar->getData(); } if( !empty($planetsData = $this->getPlanetsData()) ){ - $systemData->planets = $planetsData; + $data->planets = $planetsData; + + // 'Shattered' systems have ONLY planets named with '(shattered)' + // -> system 'Thera' has '(shattered)' AND other planets -> not shattered. + // -> system 'J164104, 'J115422' - the only non-shattered wormholes which have a shattered planet -> not shattered. + $data->shattered = count(array_filter($planetsData, function(object $planetData){ + return property_exists($planetData, 'type') && + (strpos(strtolower($planetData->type->name), '(shattered)') !== false); + })) == count($planetsData); } if( !empty($staticsData = $this->getStaticsData()) ){ - $systemData->statics = $staticsData; + $data->statics = $staticsData; } if( !empty($stargatesData = $this->getStargatesData()) ){ - $systemData->stargates = $stargatesData; + $data->stargates = $stargatesData; } - return $systemData; + if( !empty($stationsData = $this->getStationsData()) ){ + $data->stations = $stationsData; + } + + return $data; } /** @@ -185,11 +193,18 @@ class SystemModel extends AbstractUniverseModel { } $this->trueSec = $trueSec; // set 'security' for NON wormhole systems! -> those get updated from csv import - if(!preg_match('/^j\d+$/i', $this->name)){ - // check for "Abyssal" system - if( - $this->get('constellationId', true) >= 22000001 && - $this->get('constellationId', true) <= 22000025 + // 'J1226-0' is also a wormhole with a '-' in the name! (single system) + if( + !preg_match('/^j(\d{6}|\d{4}-\d)$/i', $this->name) && + $this->name != 'Thera' + ){ + $constellationId = (int)$this->get('constellationId', true); + if($constellationId == 23000001){ + // "Pocket" system + $security = 'P'; + }elseif( + $constellationId >= 22000001 && + $constellationId <= 22000025 ){ // "Abyssal" system $security = 'A'; @@ -220,30 +235,141 @@ class SystemModel extends AbstractUniverseModel { } /** - * setter for positions array (x/y/z) - * @param $position - * @return null + * @param array $sovData + * @return bool true if sovereignty data changed */ - public function set_position($position){ - $position = (array)$position; - if(count($position) === 3){ - $this->x = $position['x']; - $this->y = $position['y']; - $this->z = $position['z']; + public function updateSovereigntyData(array $sovData = []) : bool { + $hasChanged = false; + $systemId = (int)$sovData['systemId']; + $factionId = (int)$sovData['factionId']; + $allianceId = (int)$sovData['allianceId']; + $corporationId = (int)$sovData['corporationId']; + + if($this->valid()){ + if($systemId === $this->_id){ + // sov data belongs to this system + $validSovData = (bool)max($factionId, $allianceId, $corporationId); + if($validSovData){ + // at least one of these Ids must exist for a sovereignty relation + /** + * @var $sovereignty SovereigntyMapModel + */ + if(!$sovereignty = $this->sovereignty){ + // insert new sovereignty data + $sovereignty = $this->rel('sovereignty'); + } + + $sovData['systemId'] = $this; + + if($factionId){ + // HS, L - systems have "faction war" + $sovData['allianceId'] = null; + $sovData['corporationId'] = null; + + /** + * @var $faction FactionModel + */ + $faction = $sovereignty->rel('factionId'); + $faction->loadById($factionId); + $sovData['factionId'] = $faction; + }else{ + // 0.0 - systems have sovereignty data by corp and/or ally + $sovData['factionId'] = null; + + /** + * @var $alliance AllianceModel|null + */ + $alliance = null; + if($allianceId){ + $alliance = $sovereignty->rel('allianceId'); + $alliance->loadById($allianceId); + } + + /** + * @var $corporation CorporationModel|null + */ + $corporation = null; + if($corporationId){ + $corporation = $sovereignty->rel('corporationId'); + $corporation->loadById($corporationId); + } + + $sovData['allianceId'] = $alliance; + $sovData['corporationId'] = $corporation; + } + + $sovereignty->copyfrom($sovData, ['systemId', 'factionId', 'allianceId', 'corporationId']); + + // must be called before save(). Changed fields get reset after save() is called! + if($sovereignty->changed()){ + $hasChanged = true; + } + + $sovereignty->save(); + }elseif($this->sovereignty){ + // delete existing sov data + // -> hint: WH - systems never have sovereignty data + $this->sovereignty->erase(); + $hasChanged = true; + } + } } - return null; + + return $hasChanged; } /** - * setter for static systems (wormholes) - * -> comma separated string or array - * @param $staticNames - * @return null + * @param array $fwData + * @return bool true if faction warfare data changed */ - public function set_staticNames($staticNames){ - $staticNames = array_unique(is_string($staticNames) ? explode(',', $staticNames) : (array)$staticNames); - $this->virtual('staticNames', array_map('strtoupper', $staticNames)); - return null; + public function updateFactionWarData(array $fwData = []) : bool { + $hasChanged = false; + $systemId = (int)$fwData['systemId']; + $ownerFactionId = (int)$fwData['ownerFactionId']; + $occupierFactionId = (int)$fwData['occupierFactionId']; + + if($this->valid()){ + if($systemId === $this->_id){ + /** + * @var $factionWar FactionWarSystemModel + */ + if(!$factionWar = $this->factionWar){ + // insert new faction war data + $factionWar = $this->rel('factionWar'); + } + + $fwData['systemId'] = $this; + + if($ownerFactionId){ + /** + * @var $ownerFaction FactionModel + */ + $ownerFaction = $factionWar->rel('ownerFactionId'); + $ownerFaction->loadById($ownerFactionId); + $fwData['ownerFactionId'] = $ownerFaction; + } + + if($occupierFactionId){ + /** + * @var $occupierFaction FactionModel + */ + $occupierFaction = $factionWar->rel('occupierFactionId'); + $occupierFaction->loadById($occupierFactionId); + $fwData['occupierFactionId'] = $occupierFaction; + } + + $factionWar->copyfrom($fwData, ['systemId', 'ownerFactionId', 'occupierFactionId', 'contested', 'victoryPoints', 'victoryPointsThreshold']); + + // must be called before save(). Changed fields get reset after save() is called! + if($factionWar->changed()){ + $hasChanged = true; + } + + $factionWar->save(); + } + } + + return $hasChanged; } /** @@ -253,38 +379,8 @@ class SystemModel extends AbstractUniverseModel { * @param $pkeys */ public function afterUpdateEvent($self, $pkeys){ - $staticNames = (array)$self->staticNames; - - if( - count($staticNames) > 0 && // make sure statics are set. In case a wh system get updated without statics are set - preg_match('/^c\d+$/i', $self->security) // make sure it is a wormhole - ){ - foreach((array)$self->statics as $static){ - if(in_array($static->wormholeId->name, $staticNames)){ - unset($staticNames[array_search($static->wormholeId->name, $staticNames)]); - }else{ - $static->erase(); - } - } - - // add new statics - foreach($staticNames as $staticName){ - $static = $self->rel('statics'); - /** - * @var $wormhole WormholeModel - */ - $wormhole = $static->rel('wormholeId')->getByForeignKey('name', $staticName, ['limit' => 1]); - if( !$wormhole->dry() ){ - $static->systemId = $self; - $static->wormholeId = $wormhole; - $static->save(); - } - } - } - // build search index $self->buildIndex(); - return parent::afterUpdateEvent($self, $pkeys); } @@ -292,14 +388,14 @@ class SystemModel extends AbstractUniverseModel { * get data from all planets * @return array */ - protected function getPlanetsData(){ + protected function getPlanetsData() : array { $planetsData = []; if($this->planets){ + /** + * @var $planet PlanetModel + */ foreach($this->planets as &$planet){ - /** - * @var $planet PlanetModel - */ $planetsData[] = $planet->getData(); } } @@ -310,14 +406,14 @@ class SystemModel extends AbstractUniverseModel { * get data from all static wormholes * @return array */ - protected function getStaticsData(){ + protected function getStaticsData() : array { $staticsData = []; if($this->statics){ + /** + * @var $static SystemStaticModel + */ foreach($this->statics as &$static){ - /** - * @var $static SystemStaticModel - */ $staticsData[] = $static->getData(); } } @@ -328,25 +424,62 @@ class SystemModel extends AbstractUniverseModel { * get data from all stargates * @return array */ - protected function getStargatesData(){ + protected function getStargatesData() : array { $stargatesData = []; if($this->stargates){ + /** + * @var $stargate StargateModel + */ foreach($this->stargates as &$stargate){ - /** - * @var $stargate StargateModel - */ $stargatesData[] = $stargate->getData(); } } return $stargatesData; } + /** + * get data from all stations + * @return array + */ + protected function getStationsData() : array { + $stationsData = []; + + if($this->stations){ + /** + * @var $station StationModel + */ + foreach($this->stations as &$station){ + $data = $station->getData(); + if(!$data->race){ + // should never happen NPC stations always have a owning race + $data->race = (object) []; + $data->race->id = 0; + $data->race->name = 'unknown'; + $data->race->faction = (object) []; + $data->race->faction->id = 0; + $data->race->faction->name = 'unknown'; + } + + if(!array_key_exists($data->race->faction->id, $stationsData)){ + $stationsData[$data->race->faction->id] = [ + 'id' => $data->race->faction->id, + 'name' => $data->race->name, + 'stations' => [] + ]; + } + + $stationsData[$data->race->faction->id]['stations'][] = $data; + } + } + return $stationsData; + } + /** * update system from ESI */ public function updateModel(){ - if( !$this->dry() ){ + if($this->valid()){ $this->loadData($this->_id); $this->loadPlanetsData(); } @@ -387,7 +520,7 @@ class SystemModel extends AbstractUniverseModel { * load planets data for this system */ public function loadPlanetsData(){ - if( !$this->dry() ){ + if($this->valid()){ $data = self::getF3()->ccpClient()->getUniverseSystemData($this->_id); if($data['planets']){ // planets are optional since ESI v4 (e.g. Abyssal systems) @@ -408,9 +541,9 @@ class SystemModel extends AbstractUniverseModel { * -> stargates to destination system which is not in DB get ignored */ public function loadStargatesData(){ - if( !$this->dry() ){ + if($this->valid()){ $data = self::getF3()->ccpClient()->getUniverseSystemData($this->_id); - if(!empty($data)){ + if($data['stargates']){ foreach((array)$data['stargates'] as $stargateId){ /** * @var $stargate StargateModel @@ -422,4 +555,23 @@ class SystemModel extends AbstractUniverseModel { } } } + + /** + * load NPC owned stations for this system + */ + public function loadStationsData(){ + if($this->valid()){ + $data = self::getF3()->ccpClient()->getUniverseSystemData($this->_id); + if($data['stations']){ + foreach((array)$data['stations'] as $stationId){ + /** + * @var $station SystemModel + */ + $station = $this->rel('stations'); + $station->loadById($stationId); + $station->reset(); + } + } + } + } } \ No newline at end of file diff --git a/app/main/model/universe/systemneighbourmodel.php b/app/main/model/universe/systemneighbourmodel.php new file mode 100644 index 00000000..f598565c --- /dev/null +++ b/app/main/model/universe/systemneighbourmodel.php @@ -0,0 +1,92 @@ + used on /setup page or index build + * @var bool + */ + protected $allowTruncate = true; + + /** + * @var array + */ + protected $fieldConf = [ + 'regionId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Model\Universe\RegionModel', + 'constraint' => [ + [ + 'table' => 'region', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'constellationId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Model\Universe\ConstellationModel', + 'constraint' => [ + [ + 'table' => 'constellation', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'systemId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'unique' => true, + 'belongs-to-one' => 'Model\Universe\SystemModel', + 'constraint' => [ + [ + 'table' => 'system', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'systemName' => [ + 'type' => Schema::DT_VARCHAR128, + 'nullable' => false, + 'default' => '' + ], + 'jumpNodes' => [ + 'type' => Schema::DT_VARCHAR512, + 'nullable' => false, + 'default' => '' + ], + 'trueSec' => [ + 'type' => Schema::DT_DECIMAL, + 'nullable' => false, + 'default' => 0 + ] + ]; + + /** + * No static columns added + * @var bool + */ + protected $addStaticFields = false; + + /** + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){} +} \ No newline at end of file diff --git a/app/main/model/universe/systemstaticmodel.php b/app/main/model/universe/systemstaticmodel.php index f736b781..a072b123 100644 --- a/app/main/model/universe/systemstaticmodel.php +++ b/app/main/model/universe/systemstaticmodel.php @@ -12,8 +12,14 @@ use DB\SQL\Schema; class SystemStaticModel extends AbstractUniverseModel { + /** + * @var string + */ protected $table = 'system_static'; + /** + * @var array + */ protected $fieldConf = [ 'systemId' => [ 'type' => Schema::DT_INT, @@ -27,13 +33,13 @@ class SystemStaticModel extends AbstractUniverseModel { ], 'validate' => 'notDry' ], - 'wormholeId' => [ + 'typeId' => [ 'type' => Schema::DT_INT, 'index' => true, - 'belongs-to-one' => 'Model\Universe\WormholeModel', + 'belongs-to-one' => 'Model\Universe\TypeModel', 'constraint' => [ [ - 'table' => 'wormhole', + 'table' => 'type', 'on-delete' => 'CASCADE' ] ], @@ -52,8 +58,14 @@ class SystemStaticModel extends AbstractUniverseModel { * @return null|string */ public function getData(){ - return $this->wormholeId ? $this->wormholeId->name : null; + return $this->typeId ? $this->typeId->getWormholeName() : null; } + + /** + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){} /** @@ -66,7 +78,7 @@ class SystemStaticModel extends AbstractUniverseModel { */ public static function setup($db = null, $table = null, $fields = null){ if($status = parent::setup($db, $table, $fields)){ - $status = parent::setMultiColumnIndex(['systemId', 'wormholeId'], true); + $status = parent::setMultiColumnIndex(['systemId', 'typeId'], true); } return $status; } diff --git a/app/main/model/universe/typeattributemodel.php b/app/main/model/universe/typeattributemodel.php new file mode 100644 index 00000000..eec9d4bf --- /dev/null +++ b/app/main/model/universe/typeattributemodel.php @@ -0,0 +1,92 @@ + [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Model\Universe\TypeModel', + 'constraint' => [ + [ + 'table' => 'type', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'attributeId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Model\Universe\DogmaAttributeModel', + 'constraint' => [ + [ + 'table' => 'dogma_attribute', + 'on-delete' => 'CASCADE' + ] + ], + 'validate' => 'notDry' + ], + 'value' => [ + 'type' => Schema::DT_FLOAT, + 'nullable' => false, + 'default' => 0 + ] + ]; + + /** + * No static columns added + * @var bool + */ + protected $addStaticFields = false; + + /** + * @return \stdClass + */ + public function getData(){ + $typeAttributeData = $this->attributeId->getData(); + $typeAttributeData->value = (float)$this->value; + + return $typeAttributeData; + } + + /** + * @param int $id + * @param string $accessToken + * @param array $additionalOptions + */ + protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){} + + /** + * overwrites parent + * @param null $db + * @param null $table + * @param null $fields + * @return bool + * @throws \Exception + */ + public static function setup($db = null, $table = null, $fields = null){ + if($status = parent::setup($db, $table, $fields)){ + $status = parent::setMultiColumnIndex(['typeId', 'attributeId'], true); + } + return $status; + } +} \ No newline at end of file diff --git a/app/main/model/universe/typemodel.php b/app/main/model/universe/typemodel.php index 5401b4f2..4402d741 100644 --- a/app/main/model/universe/typemodel.php +++ b/app/main/model/universe/typemodel.php @@ -9,11 +9,31 @@ namespace Model\Universe; use DB\SQL\Schema; +use lib\Config; +use lib\Util; class TypeModel extends AbstractUniverseModel { - protected $table = 'type'; + /** + * @var string + */ + protected $table = 'type'; + /** + * default store option for 'dogma' typeAttributes data + * -> set to true will store all typeAttributes from ESI for a type + * -> should be enabled for specific types, where data is used by Pathfinder + */ + const DEFAULT_STORE_DOGMA_ATTRIBUTES = false; + + /** + * @var bool + */ + public $storeDogmaAttributes = self::DEFAULT_STORE_DOGMA_ATTRIBUTES; + + /** + * @var array + */ protected $fieldConf = [ 'name' => [ 'type' => Schema::DT_VARCHAR128, @@ -83,6 +103,9 @@ class TypeModel extends AbstractUniverseModel { 'default' => 0, 'index' => true ], + 'stations' => [ + 'has-many' => ['Model\Universe\StationModel', 'typeId'] + ], 'structures' => [ 'has-many' => ['Model\Universe\StructureModel', 'typeId'] ], @@ -92,11 +115,46 @@ class TypeModel extends AbstractUniverseModel { 'stars' => [ 'has-many' => ['Model\Universe\StarModel', 'typeId'] ], - 'wormholes' => [ - 'has-many' => ['Model\Universe\WormholeModel', 'typeId'] + 'attributes' => [ + 'has-many' => ['Model\Universe\TypeAttributeModel', 'typeId'] + ], + 'stargates' => [ + 'has-many' => ['Model\Universe\StargateModel', 'typeId'] + ], + 'statics' => [ + 'has-many' => ['Model\Universe\SystemStaticModel', 'typeId'] ] ]; + /** + * set 'dogma_attributes' during ESI import process to a virtual field + * -> 'dogma_attributes' get imported after type is saved + * @see loadData() + * @param $dogmaAttributesData + * @return null + */ + public function set_dogma_attributes($dogmaAttributesData){ + $this->virtual('dogmaAttributes', (array)$dogmaAttributesData); + return null; + } + + /** + * special getter for 'wormhole' types + * @return string|null + */ + public function getWormholeName(){ + return self::formatWormholeName($this->name); + } + + /** + * @param bool $mapper + * @return NULL|void + */ + public function reset($mapper = true){ + $this->clearVirtual('dogmaAttributes'); + parent::reset($mapper); + } + /** * get type data * @param array $additionalData @@ -104,24 +162,70 @@ class TypeModel extends AbstractUniverseModel { */ public function getData(array $additionalData = []){ $typeData = (object) []; - $typeData->id = $this->id; + $typeData->id = $this->_id; $typeData->name = $this->name; foreach($additionalData as $key){ - $typeData->$key = $this->$key; + if($key == 'attributes'){ + // add 'dogma' typeAttributes data + $typeData->$key = $this->getAttributesData(); + }elseif($this->exists($key)){ + $typeData->$key = $this->$key; + } } return $typeData; } + /** + * get wormholeData from object + * @return \stdClass + */ + public function getWormholeData() : \stdClass { + $wormholeData = (object) []; + if($this->valid()){ + $wormholeData->name = $this->getWormholeName(); + $wormholeData->static = $this->statics ? (bool)count($this->statics) : false; + $wormholeData->security = ''; + $wormholeData->massTotal = null; + $wormholeData->massIndividual = null; + $wormholeData->maxStableTime = null; + foreach($this->getAttributesData() as $id => $attributesData){ + switch($id){ + case 1381: // 'wormholeTargetSystemClass' -> 'security' + $wormholeData->security = self::getSystemSecurityFromId((int)$attributesData['value']); + break; + case 1383: // 'wormholeMaxStableMass' -> 'massTotal' + $wormholeData->massTotal = $attributesData['value']; + break; + case 1385: // 'wormholeMaxJumpMass' -> 'massIndividual' + $wormholeData->massIndividual = $attributesData['value']; + break; + case 1384: // 'wormholeMassRegeneration' -> 'massRegeneration' + if($attributesData['value']){ + $wormholeData->massRegeneration = $attributesData['value']; + } + break; + case 1382: // 'wormholeMaxStableTime' -> 'maxStableTime' + $wormholeData->maxStableTime = $attributesData['value'] / 60; + break; + case Config::ESI_DOGMA_ATTRIBUTE_SCANWHSTRENGTH_ID: // 'scanWormholeStrength' -> 'scanWormholeStrength' + $wormholeData->scanWormholeStrength = $attributesData['value']; + break; + } + } + } + return $wormholeData; + } + /** * get shipData from object * -> more fields can be added in here if needed * @return \stdClass */ - public function getShipData(): \stdClass { + public function getShipData() : \stdClass { $shipData = (object) []; - if(!$this->dry()){ + if($this->valid()){ $shipData->typeId = $this->_id; $shipData->typeName = $this->name; $shipData->mass = $this->mass; @@ -129,6 +233,118 @@ class TypeModel extends AbstractUniverseModel { return $shipData; } + /** + * @return array + */ + protected function getAttributesData() : array { + $attributesData = []; + + if($this->attributes){ + foreach($this->attributes as $typeAttribute){ + /** + * @var $typeAttribute TypeAttributeModel + */ + $attributesData[] = get_object_vars($typeAttribute->getData()); + } + } + return Util::arrayGetBy($attributesData, 'id'); + } + + /** + * Event "Hook" function + * return false will stop any further action + * @param self $self + * @param $pkeys + */ + public function afterInsertEvent($self, $pkeys){ + $self->syncDogmaAttributes(); + + return parent::afterInsertEvent($self, $pkeys); + } + + /** + * Event "Hook" function + * @param self $self + * @param $pkeys + */ + public function afterUpdateEvent($self, $pkeys){ + $self->syncDogmaAttributes(); + + return parent::afterUpdateEvent($self, $pkeys); + } + + /** + * sync existing 'dogma' typeAttributes data with "new/updated" typeAttributes + * -> $this->dogmaAttributes must be set before calling this method + */ + protected function syncDogmaAttributes(){ + if( + $this->storeDogmaAttributes && + !empty($dogmaAttributesData = (array)$this->dogmaAttributes) + ){ + foreach((array)$this->attributes as $typeAttribute){ + $key = array_search($typeAttribute->get('attributeId', true), array_column($dogmaAttributesData, 'attributeId')); + if($key !== false){ + // attribute still belongs to this 'type' -> update value + $typeAttribute->copyfrom($dogmaAttributesData[$key], ['value']); + $typeAttribute->save(); + + unset($dogmaAttributesData[$key]); + $dogmaAttributesData = array_values($dogmaAttributesData); + }else{ + // attribute no longer belongs to this 'type' + $typeAttribute->erase(); + } + } + + // add new dogmaTypes + foreach($dogmaAttributesData as $dogmaAttributeData){ + /** + * @var $typeAttribute TypeAttributeModel + * @var $dogmaAttribute DogmaAttributeModel + */ + $typeAttribute = $this->rel('attributes'); + $dogmaAttribute = $typeAttribute->rel('attributeId'); + $dogmaAttribute->loadById($dogmaAttributeData['attributeId']); + if($dogmaAttribute->valid()){ + $typeAttribute->typeId = $this; + $typeAttribute->attributeId = $dogmaAttribute; + $typeAttribute->value = $dogmaAttributeData['value']; + $typeAttribute->save(); + } + } + } + } + + /** + * manipulate 'dogma_attributes' array be reference + * -> used to inject custom attributes (not available from ESI) + * @param array $data + */ + private function manipulateDogmaAttributes(array &$data){ + if(!$this->storeDogmaAttributes){ + // attributes should not get saved + unset($data['dogma_attributes']); + }elseif(!empty($data['dogma_attributes'])){ + switch($data['groupId']){ + case Config::ESI_GROUP_WORMHOLE_ID: + if( + !empty($wormholesCSVData = static::getCSVData('wormhole', 'name')) && + !empty($wormholeCSVData = $wormholesCSVData[self::formatWormholeName($data['name'])]) + ){ + // found relevant wormhole data in *.csv for current type + if(!empty($scanWormholeStrength = (float)$wormholeCSVData['scanWormholeStrength'])){ + $data['dogma_attributes'][] = [ + 'attributeId' => Config::ESI_DOGMA_ATTRIBUTE_SCANWHSTRENGTH_ID, + 'value' => $scanWormholeStrength + ]; + } + } + break; + } + } + } + /** * load data from API into $this and save $this * @param int $id @@ -138,6 +354,8 @@ class TypeModel extends AbstractUniverseModel { protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){ $data = self::getF3()->ccpClient()->getUniverseTypesData($id); if(!empty($data)){ + $this->manipulateDogmaAttributes($data); + /** * @var $group GroupModel */ @@ -149,4 +367,12 @@ class TypeModel extends AbstractUniverseModel { $this->save(); } } + + /** + * @param string|null $name + * @return string|null + */ + public static function formatWormholeName(?string $name) : ?string { + return (!empty($name) && !empty($format = @end(explode(' ', $name)))) ? $format : null; + } } \ No newline at end of file diff --git a/app/main/model/universe/wormholemodel.php b/app/main/model/universe/wormholemodel.php deleted file mode 100644 index 93d257a0..00000000 --- a/app/main/model/universe/wormholemodel.php +++ /dev/null @@ -1,192 +0,0 @@ - [ - 'type' => Schema::DT_VARCHAR128, - 'nullable' => false, - 'default' => '', - 'index' => true, - 'unique' => true - ], - 'typeId' => [ - 'type' => Schema::DT_INT, - 'index' => true, - 'belongs-to-one' => 'Model\Universe\TypeModel', - 'constraint' => [ - [ - 'table' => 'type', - 'on-delete' => 'SET NULL' - ] - ], - 'validate' => 'notDry' - ], - 'static' => [ - 'type' => Schema::DT_BOOL, - 'nullable' => false, - 'default' => 0 - ], - 'security' => [ - 'type' => Schema::DT_VARCHAR128, - 'nullable' => false, - 'default' => '' - ], - 'massTotal' => [ - 'type' => Schema::DT_BIGINT, - 'nullable' => true, - 'default' => null - ], - 'massIndividual' => [ - 'type' => Schema::DT_BIGINT, - 'nullable' => true, - 'default' => null - ], - 'massRegeneration' => [ - 'type' => Schema::DT_BIGINT, - 'nullable' => true, - 'default' => null - ], - 'maxStableTime' => [ - 'type' => Schema::DT_TINYINT, - 'nullable' => true, - 'default' => null - ], - 'signatureStrength' => [ - 'type' => Schema::DT_FLOAT, - 'nullable' => true, - 'default' => null - ], - 'systems' => [ - 'has-many' => ['Model\Universe\SystemStaticModel', 'wormholeId'] - ] - ]; - - /** - * get wormhole data - * @return \stdClass - */ - public function getData(){ - - $wormholeData = (object) []; - $wormholeData->name = $this->name; - $wormholeData->static = $this->static; - $wormholeData->security = $this->security; - $wormholeData->massTotal = $this->massTotal; - $wormholeData->massIndividual = $this->massIndividual; - - if($this->massRegeneration){ - $wormholeData->massRegeneration = $this->massRegeneration; - } - - $wormholeData->maxStableTime = $this->maxStableTime; - - // signature strength as defined by http://wiki.eve-inspiracy.com/index.php?title=Wormhole_Signature_Strength_List - if($this->signatureStrength){ - $wormholeData->signatureStrength = $this->signatureStrength; - } - - return $wormholeData; - } - - /** - * setter for typeId - * @param string $typeId - * @return string|int|null - */ - public function set_typeId($typeId){ - if(!is_object($typeId)){ - /** - * @var $type TypeModel - */ - $type = $this->rel('typeId'); - $type->loadById((int)$typeId); - $typeId = $type->dry() ? null : $type->_id; - } - return $typeId; - } - - /** - * setter for massTotal - * @param string $mass - * @return int|null - */ - public function set_massTotal($mass){ - $mass = (int)$mass; - return $mass ? : null; - } - - /** - * setter for massIndividual - * @param string $mass - * @return string|null - */ - public function set_massIndividual($mass){ - $mass = (int)$mass; - return $mass ? : null; - } - - /** - * setter for massRegeneration - * @param $mass - * @return int|null - */ - public function set_massRegeneration($mass){ - $mass = (int)$mass; - return $mass ? : null; - } - - /** - * setter for maxStableTime - * @param string $hours - * @return int|null - */ - public function set_maxStableTime($hours){ - $hours = (int)$hours; - return $hours ? : null; - } - - /** - * setter for signatureStrength - * @param string $strength - * @return float|null - */ - public function set_signatureStrength($strength){ - $strength = (float)$strength; - return $strength ? : null; - } - - /** - * @param array $fields - * @return bool - */ - public function exportData(array $fields = [ - 'id', 'name', 'typeId', 'static', 'security', 'massTotal', 'massIndividual', - 'massRegeneration', 'maxStableTime', 'signatureStrength'] - ) : bool { - return parent::exportData($fields); - } - - /** - * @param int $id - * @param string $accessToken - * @param array $additionalOptions - */ - protected function loadData(int $id, string $accessToken = '', array $additionalOptions = []){} - -} \ No newline at end of file diff --git a/app/pathfinder.ini b/app/pathfinder.ini index 569b14e1..8f98fe4f 100644 --- a/app/pathfinder.ini +++ b/app/pathfinder.ini @@ -13,8 +13,8 @@ NAME = Pathfinder ; Version is used for CSS/JS cache busting and is part of the URL for static resources: ; e.g. public/js/vX.X.X/app.js ; Syntax: String (current version) -; Default: v1.5.0 -VERSION = v1.5.3 +; Default: v1.5.4 +VERSION = v1.5.4 ; Contact information [optional] ; Shown on 'licence', 'contact' page. @@ -377,6 +377,9 @@ LOG_LINES = 1000 [PATHFINDER.API] CCP_IMAGE_SERVER = https://image.eveonline.com Z_KILLBOARD = https://zkillboard.com/api +EVEEYE = https://eveeye.com +DOTLAN = http://evemaps.dotlan.net +ANOIK = http://anoik.is ; GitHub Developer API GIT_HUB = https://api.github.com diff --git a/app/requirements.ini b/app/requirements.ini index 35525f68..412b8fea 100644 --- a/app/requirements.ini +++ b/app/requirements.ini @@ -56,12 +56,13 @@ PDO_TIMEOUT = 2 ; MySql variables. Values are auto. set as 'SESSION' vars ; https://dev.mysql.com/doc/refman/5.5/en/show-variables.html DEFAULT_STORAGE_ENGINE = InnoDB -CHARACTER_SET_DATABASE = utf8 -CHARACTER_SET_CLIENT = utf8 -CHARACTER_SET_RESULTS = utf8 -CHARACTER_SET_CONNECTION = utf8 -COLLATION_DATABASE = utf8_general_ci -COLLATION_CONNECTION = utf8_general_ci +CHARACTER_SET_SERVER = utf8mb4 +CHARACTER_SET_DATABASE = utf8mb4 +CHARACTER_SET_CLIENT = utf8mb4 +CHARACTER_SET_RESULTS = utf8mb4 +CHARACTER_SET_CONNECTION = utf8mb4 +COLLATION_DATABASE = utf8mb4_unicode_ci +COLLATION_CONNECTION = utf8mb4_unicode_ci FOREIGN_KEY_CHECKS = ON INNODB_FILE_PER_TABLE = ON WAIT_TIMEOUT = 28800 @@ -80,6 +81,4 @@ NODE = 6.0 NPM = 3.10.0 [REQUIREMENTS.DATA] -STRUCTURES = 33 -SHIPS = 491 NEIGHBOURS = 5201 diff --git a/composer.json b/composer.json index 34056062..40fa6e9b 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "react/socket": "1.2.*", "react/promise-stream": "1.1.*", "clue/ndjson-react": "1.0.*", - "exodus4d/pathfinder_esi": "v1.3.1" + "exodus4d/pathfinder_esi": "v1.3.2" }, "suggest": { "ext-redis": "Redis can be used as cache backend." diff --git a/composer.lock b/composer.lock index 76ff5629..7c1b92cf 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ad512ee5d1242b7e46b3620b1b05a2a2", + "content-hash": "cd6689b64084cbe1bdbf74b1165fd472", "packages": [ { "name": "cache/adapter-common", @@ -55,7 +55,7 @@ { "name": "Tobias Nyholm", "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/nyholm" + "homepage": "https://github.com/Nyholm" } ], "description": "Common classes for PSR-6 adapters", @@ -123,7 +123,7 @@ { "name": "Tobias Nyholm", "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/nyholm" + "homepage": "https://github.com/Nyholm" } ], "description": "A PSR-6 cache implementation using a php array. This implementation supports tags", @@ -191,7 +191,7 @@ { "name": "Tobias Nyholm", "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/nyholm" + "homepage": "https://github.com/Nyholm" } ], "description": "A PSR-6 cache implementation using filesystem. This implementation supports tags", @@ -253,7 +253,7 @@ { "name": "Tobias Nyholm", "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/nyholm" + "homepage": "https://github.com/Nyholm" } ], "description": "A helper trait and interface to your PSR-6 cache to support hierarchical keys.", @@ -311,7 +311,7 @@ { "name": "Tobias Nyholm", "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/nyholm" + "homepage": "https://github.com/Nyholm" } ], "description": "A decorator that makes your cache support namespaces", @@ -381,7 +381,7 @@ { "name": "Tobias Nyholm", "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/nyholm" + "homepage": "https://github.com/Nyholm" } ], "description": "A PSR-6 cache implementation using Redis (PhpRedis). This implementation supports tags", @@ -432,7 +432,7 @@ { "name": "Tobias Nyholm", "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/nyholm" + "homepage": "https://github.com/Nyholm" }, { "name": "Nicolas Grekas", @@ -505,7 +505,7 @@ { "name": "Tobias Nyholm", "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/nyholm" + "homepage": "https://github.com/Nyholm" } ], "description": "A PSR-6 cache implementation using Void. This implementation supports tags", @@ -629,28 +629,30 @@ }, { "name": "doctrine/lexer", - "version": "1.0.2", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "1febd6c3ef84253d7c815bed85fc622ad207a9f8" + "reference": "e17f069ede36f7534b95adec71910ed1b49c74ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/1febd6c3ef84253d7c815bed85fc622ad207a9f8", - "reference": "1febd6c3ef84253d7c815bed85fc622ad207a9f8", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/e17f069ede36f7534b95adec71910ed1b49c74ea", + "reference": "e17f069ede36f7534b95adec71910ed1b49c74ea", "shasum": "" }, "require": { - "php": ">=5.3.2" + "php": "^7.2" }, "require-dev": { - "phpunit/phpunit": "^4.5" + "doctrine/coding-standard": "^6.0", + "phpstan/phpstan": "^0.11.8", + "phpunit/phpunit": "^8.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { @@ -663,14 +665,14 @@ "MIT" ], "authors": [ - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, { "name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com" }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, { "name": "Johannes Schmitt", "email": "schmittjoh@gmail.com" @@ -685,20 +687,20 @@ "parser", "php" ], - "time": "2019-06-08T11:03:04+00:00" + "time": "2019-07-30T19:33:28+00:00" }, { "name": "egulias/email-validator", - "version": "2.1.9", + "version": "2.1.11", "source": { "type": "git", "url": "https://github.com/egulias/EmailValidator.git", - "reference": "128cc721d771ec2c46ce59698f4ca42b73f71b25" + "reference": "92dd169c32f6f55ba570c309d83f5209cefb5e23" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/128cc721d771ec2c46ce59698f4ca42b73f71b25", - "reference": "128cc721d771ec2c46ce59698f4ca42b73f71b25", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/92dd169c32f6f55ba570c309d83f5209cefb5e23", + "reference": "92dd169c32f6f55ba570c309d83f5209cefb5e23", "shasum": "" }, "require": { @@ -708,7 +710,8 @@ "require-dev": { "dominicsayers/isemail": "dev-master", "phpunit/phpunit": "^4.8.35||^5.7||^6.0", - "satooshi/php-coveralls": "^1.0.1" + "satooshi/php-coveralls": "^1.0.1", + "symfony/phpunit-bridge": "^4.4@dev" }, "suggest": { "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" @@ -716,7 +719,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "2.1.x-dev" } }, "autoload": { @@ -742,7 +745,7 @@ "validation", "validator" ], - "time": "2019-06-23T10:14:27+00:00" + "time": "2019-08-13T17:33:27+00:00" }, { "name": "evenement/evenement", @@ -789,16 +792,16 @@ }, { "name": "exodus4d/pathfinder_esi", - "version": "v1.3.1", + "version": "v1.3.2", "source": { "type": "git", "url": "https://github.com/exodus4d/pathfinder_esi.git", - "reference": "392cb81f1efbd9bf69d7d943f4aefdcf3f0a617c" + "reference": "7eecfe27bb15fab5142aceeee30e07355d660773" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/exodus4d/pathfinder_esi/zipball/392cb81f1efbd9bf69d7d943f4aefdcf3f0a617c", - "reference": "392cb81f1efbd9bf69d7d943f4aefdcf3f0a617c", + "url": "https://api.github.com/repos/exodus4d/pathfinder_esi/zipball/7eecfe27bb15fab5142aceeee30e07355d660773", + "reference": "7eecfe27bb15fab5142aceeee30e07355d660773", "shasum": "" }, "require": { @@ -824,9 +827,9 @@ ], "description": "ESI API library for Pathfinder", "support": { - "source": "https://github.com/exodus4d/pathfinder_esi/tree/v1.3.1" + "source": "https://github.com/exodus4d/pathfinder_esi/tree/v1.3.2" }, - "time": "2019-05-07T11:53:44+00:00" + "time": "2019-10-08T10:20:37+00:00" }, { "name": "guzzlehttp/guzzle", @@ -1017,16 +1020,16 @@ }, { "name": "league/flysystem", - "version": "1.0.53", + "version": "1.0.55", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "08e12b7628f035600634a5e76d95b5eb66cea674" + "reference": "33c91155537c6dc899eacdc54a13ac6303f156e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/08e12b7628f035600634a5e76d95b5eb66cea674", - "reference": "08e12b7628f035600634a5e76d95b5eb66cea674", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/33c91155537c6dc899eacdc54a13ac6303f156e6", + "reference": "33c91155537c6dc899eacdc54a13ac6303f156e6", "shasum": "" }, "require": { @@ -1097,20 +1100,20 @@ "sftp", "storage" ], - "time": "2019-06-18T20:09:29+00:00" + "time": "2019-08-24T11:17:19+00:00" }, { "name": "league/html-to-markdown", - "version": "4.8.1", + "version": "4.8.2", "source": { "type": "git", "url": "https://github.com/thephpleague/html-to-markdown.git", - "reference": "250d1bf45f80d15594fb6b316df777d6d4c97ad1" + "reference": "e747489191f8e9144a7270eb61f8b9516e99e413" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/250d1bf45f80d15594fb6b316df777d6d4c97ad1", - "reference": "250d1bf45f80d15594fb6b316df777d6d4c97ad1", + "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/e747489191f8e9144a7270eb61f8b9516e99e413", + "reference": "e747489191f8e9144a7270eb61f8b9516e99e413", "shasum": "" }, "require": { @@ -1142,17 +1145,17 @@ "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" + }, + { + "name": "Nick Cernis", + "email": "nick@cern.is", + "homepage": "http://modernnerd.net", + "role": "Original Author" } ], "description": "An HTML-to-markdown conversion helper for PHP", @@ -1161,20 +1164,20 @@ "html", "markdown" ], - "time": "2018-12-24T17:21:44+00:00" + "time": "2019-08-02T11:57:39+00:00" }, { "name": "monolog/monolog", - "version": "1.24.0", + "version": "1.25.1", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266" + "reference": "70e65a5470a42cfec1a7da00d30edb6e617e8dcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", - "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/70e65a5470a42cfec1a7da00d30edb6e617e8dcf", + "reference": "70e65a5470a42cfec1a7da00d30edb6e617e8dcf", "shasum": "" }, "require": { @@ -1239,7 +1242,7 @@ "logging", "psr-3" ], - "time": "2018-11-05T09:00:11+00:00" + "time": "2019-09-06T13:49:17+00:00" }, { "name": "psr/cache", @@ -1911,16 +1914,16 @@ }, { "name": "symfony/polyfill-iconv", - "version": "v1.11.0", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-iconv.git", - "reference": "f037ea22acfaee983e271dd9c3b8bb4150bd8ad7" + "reference": "685968b11e61a347c18bf25db32effa478be610f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/f037ea22acfaee983e271dd9c3b8bb4150bd8ad7", - "reference": "f037ea22acfaee983e271dd9c3b8bb4150bd8ad7", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/685968b11e61a347c18bf25db32effa478be610f", + "reference": "685968b11e61a347c18bf25db32effa478be610f", "shasum": "" }, "require": { @@ -1932,7 +1935,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11-dev" + "dev-master": "1.12-dev" } }, "autoload": { @@ -1966,20 +1969,20 @@ "portable", "shim" ], - "time": "2019-02-06T07:57:58+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.11.0", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "c766e95bec706cdd89903b1eda8afab7d7a6b7af" + "reference": "6af626ae6fa37d396dc90a399c0ff08e5cfc45b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c766e95bec706cdd89903b1eda8afab7d7a6b7af", - "reference": "c766e95bec706cdd89903b1eda8afab7d7a6b7af", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/6af626ae6fa37d396dc90a399c0ff08e5cfc45b2", + "reference": "6af626ae6fa37d396dc90a399c0ff08e5cfc45b2", "shasum": "" }, "require": { @@ -1993,7 +1996,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9-dev" + "dev-master": "1.12-dev" } }, "autoload": { @@ -2009,13 +2012,13 @@ "MIT" ], "authors": [ - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - }, { "name": "Laurent Bassin", "email": "laurent@bassin.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", @@ -2028,20 +2031,20 @@ "portable", "shim" ], - "time": "2019-03-04T13:44:35+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.11.0", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "fe5e94c604826c35a32fa832f35bd036b6799609" + "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fe5e94c604826c35a32fa832f35bd036b6799609", - "reference": "fe5e94c604826c35a32fa832f35bd036b6799609", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/b42a2f66e8f1b15ccf25652c3424265923eb4f17", + "reference": "b42a2f66e8f1b15ccf25652c3424265923eb4f17", "shasum": "" }, "require": { @@ -2053,7 +2056,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11-dev" + "dev-master": "1.12-dev" } }, "autoload": { @@ -2087,20 +2090,20 @@ "portable", "shim" ], - "time": "2019-02-06T07:57:58+00:00" + "time": "2019-08-06T08:03:45+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.11.0", + "version": "v1.12.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "ab50dcf166d5f577978419edd37aa2bb8eabce0c" + "reference": "04ce3335667451138df4307d6a9b61565560199e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/ab50dcf166d5f577978419edd37aa2bb8eabce0c", - "reference": "ab50dcf166d5f577978419edd37aa2bb8eabce0c", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/04ce3335667451138df4307d6a9b61565560199e", + "reference": "04ce3335667451138df4307d6a9b61565560199e", "shasum": "" }, "require": { @@ -2109,7 +2112,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.11-dev" + "dev-master": "1.12-dev" } }, "autoload": { @@ -2142,7 +2145,7 @@ "portable", "shim" ], - "time": "2019-02-06T07:57:58+00:00" + "time": "2019-08-06T08:03:45+00:00" } ], "packages-dev": [], diff --git a/export/csv/system_static.csv b/export/csv/system_static.csv new file mode 100644 index 00000000..ca445475 --- /dev/null +++ b/export/csv/system_static.csv @@ -0,0 +1,3773 @@ +id;systemId;typeId +1;31002604;34140 +2;31002604;34136 +3;31002587;34138 +4;31002587;34135 +5;31002587;34140 +6;31002599;34138 +7;31002599;34140 +8;31002593;34138 +9;31002593;34137 +10;31002593;34140 +11;31002601;34138 +12;31002601;34140 +13;31002586;34138 +14;31002586;34135 +15;31002586;34140 +16;31002589;34135 +17;31002589;34137 +18;31002589;34140 +19;31002603;34140 +20;31002603;34136 +21;31002585;34137 +22;31002585;34140 +23;31002585;34136 +24;31002590;34140 +25;31002590;34136 +26;31002581;34138 +27;31002581;34140 +28;31002581;34136 +29;31002591;34135 +30;31002591;34140 +31;31002591;34136 +32;31002588;34137 +33;31002588;34140 +34;31002588;34136 +35;31002583;34134 +36;31002583;34140 +37;31002594;34138 +38;31002594;34135 +39;31002594;34140 +40;31002582;34135 +41;31002582;34140 +42;31002584;34134 +43;31002584;34137 +44;31002584;34140 +45;31002600;34139 +46;31002600;34140 +47;31002595;34137 +48;31002595;34140 +49;31002602;34134 +50;31002602;34140 +51;31002597;34137 +52;31002597;34140 +53;31002598;34134 +54;31002598;34140 +55;31002596;34135 +56;31002596;34140 +57;31002580;34138 +58;31002580;34140 +59;31002592;34140 +60;31002592;34136 +61;31002572;30700 +62;31002572;30687 +63;31002518;30677 +64;31002518;30672 +65;31002518;30679 +66;31002543;30693 +67;31002543;30688 +68;31002543;30694 +69;31002528;30695 +70;31002528;30687 +71;31002535;30688 +72;31002535;30687 +73;31002530;30688 +74;31002530;30687 +75;31002557;30702 +76;31002557;30688 +77;31002548;30693 +78;31002548;30688 +79;31002548;30692 +80;31002564;30702 +81;31002564;30687 +82;31002524;30678 +83;31002524;30677 +84;31002524;30671 +85;31002514;30667 +86;31002514;30668 +87;31002542;30695 +88;31002542;30690 +89;31002542;30689 +90;31002538;30691 +91;31002538;30687 +92;31002538;30692 +93;31002511;30666 +94;31002511;30668 +95;31002529;30688 +96;31002529;30687 +97;31002525;30678 +98;31002525;30679 +99;31002525;30675 +100;31002562;30699 +101;31002562;30687 +102;31002563;30700 +103;31002563;30687 +104;31002532;30695 +105;31002532;30687 +106;31002569;30701 +107;31002569;30688 +108;31002558;30702 +109;31002558;30688 +110;31002570;30702 +111;31002570;30687 +112;31002549;30691 +113;31002549;30695 +114;31002549;30690 +115;31002537;30688 +116;31002537;30687 +117;31002513;30667 +118;31002513;30666 +119;31002539;30691 +120;31002539;30687 +121;31002539;30692 +122;31002526;30678 +123;31002526;30679 +124;31002526;30673 +125;31002512;30667 +126;31002512;30666 +127;31002574;30702 +128;31002574;30688 +129;31002561;30702 +130;31002561;30687 +131;31002571;30688 +132;31002571;30703 +133;31002567;30701 +134;31002567;30687 +135;31002546;30691 +136;31002546;30695 +137;31002546;30692 +138;31002555;30691 +139;31002555;30687 +140;31002555;30692 +141;31002552;30691 +142;31002552;30693 +143;31002552;30688 +144;31002515;30678 +145;31002515;30677 +146;31002515;30671 +147;31002568;30687 +148;31002573;30702 +149;31002573;30688 +150;31002506;30666 +151;31002506;30668 +152;31002550;30691 +153;31002550;30689 +154;31002550;30687 +155;31002516;30678 +156;31002516;30679 +157;31002516;30674 +158;31002534;30688 +159;31002534;30687 +160;31002508;30667 +161;31002508;30666 +162;31002547;30693 +163;31002547;30688 +164;31002547;30694 +165;31002523;30677 +166;31002523;30672 +167;31002523;30671 +168;31002541;30690 +169;31002541;30687 +170;31002541;30692 +171;31002576;30712 +172;31002544;30695 +173;31002544;30690 +174;31002544;30689 +175;31002509;30667 +176;31002509;30666 +177;31002556;30688 +178;31002556;30703 +179;31002579;30688 +180;31002579;30711 +181;31002554;30693 +182;31002554;30687 +183;31002554;30692 +184;31002519;30677 +185;31002519;30679 +186;31002519;30674 +187;31002536;30695 +188;31002536;30687 +189;31002553;30691 +190;31002553;30692 +191;31002551;30693 +192;31002551;30687 +193;31002551;30692 +194;31002527;30695 +195;31002527;30687 +196;31002545;30691 +197;31002545;30695 +198;31002545;30689 +199;31002559;30704 +200;31002559;30688 +201;31002559;30700 +202;31002540;30691 +203;31002540;30687 +204;31002540;30692 +205;31002510;30667 +206;31002510;30668 +207;31002507;30666 +208;31002507;30668 +209;31002577;30688 +210;31002577;30711 +211;31002565;30702 +212;31002565;30687 +213;31002522;30678 +214;31002522;30677 +215;31002575;30688 +216;31002575;30711 +217;31002521;30677 +218;31002521;30679 +219;31002521;30673 +220;31002566;30701 +221;31002533;30688 +222;31002533;30687 +223;31002517;30678 +224;31002517;30679 +225;31002517;30676 +226;31002520;30678 +227;31002520;30677 +228;31002520;30672 +229;31002560;30702 +230;31002560;30688 +231;31002531;30695 +232;31002531;30687 +233;31002505;30667 +234;31002505;30668 +235;31002578;30688 +236;31002578;30712 +237;31000001;30672 +238;31001753;30691 +239;31001753;30694 +240;31001040;30687 +241;31001338;30687 +242;31000542;30677 +243;31000542;30674 +244;31000085;30667 +245;31001383;30690 +246;31001383;30692 +247;31000805;30677 +248;31000805;30671 +249;31001684;30691 +250;31001684;30692 +251;31002253;30702 +252;31000995;30687 +253;31001907;30699 +254;31001086;30695 +255;31001638;30689 +256;31001638;30692 +257;31001428;30693 +258;31001428;30690 +259;31001169;30695 +260;31000211;30666 +261;31002076;30703 +262;31000479;30677 +263;31000479;30674 +264;31000068;30667 +265;31002132;30700 +266;31001979;30702 +267;31001334;30687 +268;31001814;30693 +269;31001814;30690 +270;31001823;30693 +271;31001823;30692 +272;31002432;30710 +273;31002052;30702 +274;31000230;30666 +275;31002039;30702 +276;31000474;30677 +277;31000474;30674 +278;31002428;30710 +279;31000678;30678 +280;31000678;30672 +281;31002016;30702 +282;31002233;30702 +283;31002190;30700 +284;31000996;30687 +285;31000460;30677 +286;31000460;30674 +287;31000852;30677 +288;31000852;30671 +289;31000859;30677 +290;31000859;30671 +291;31001892;30698 +292;31001896;30698 +293;31001044;30687 +294;31000008;30668 +295;31002146;30700 +296;31001436;30693 +297;31001436;30694 +298;31000677;30678 +299;31000677;30672 +300;31002479;30702 +301;31001692;30691 +302;31001692;30692 +303;31001794;30693 +304;31001794;30690 +305;31002246;30702 +306;31002024;30702 +307;31002135;30700 +308;31000904;30687 +309;31001683;30691 +310;31001683;30692 +311;31001414;30693 +312;31001414;30690 +313;31001977;30702 +314;31000894;30687 +315;31000173;30666 +316;31002023;30702 +317;31001655;30691 +318;31001655;30692 +319;31001209;30687 +320;31001496;30689 +321;31001496;30692 +322;31001639;30689 +323;31001639;30692 +324;31001817;30693 +325;31001817;30690 +326;31001669;30691 +327;31001669;30693 +328;31000533;30677 +329;31000533;30674 +330;31001249;30688 +331;31002476;30703 +332;31000448;30677 +333;31000448;30673 +334;31001760;30691 +335;31001760;30690 +336;31001279;30688 +337;31001615;30690 +338;31001615;30692 +339;31000076;30667 +340;31002167;30700 +341;31000640;30678 +342;31000640;30672 +343;31001168;30695 +344;31000104;30667 +345;31000663;30678 +346;31000663;30672 +347;31002308;30701 +348;31000847;30677 +349;31000847;30671 +350;31000039;30667 +351;31001118;30695 +352;31001416;30693 +353;31001416;30690 +354;31000358;30677 +355;31000358;30673 +356;31001676;30691 +357;31001676;30690 +358;31001432;30691 +359;31001432;30690 +360;31002400;30707 +361;31002161;30700 +362;31001621;30690 +363;31001621;30692 +364;31001167;30695 +365;31002361;30701 +366;31000844;30677 +367;31000844;30671 +368;31002438;30711 +369;31001440;30693 +370;31001440;30694 +371;31002462;30711 +372;31000875;30677 +373;31000875;30671 +374;31000776;30677 +375;31000776;30671 +376;31000033;30668 +377;31001103;30695 +378;31002456;30711 +379;31000927;30687 +380;31002461;30711 +381;31000067;30667 +382;31000203;30666 +383;31000777;30677 +384;31000777;30671 +385;31001311;30688 +386;31001841;30693 +387;31001841;30689 +388;31000310;30666 +389;31001143;30695 +390;31002184;30700 +391;31001524;30691 +392;31001524;30692 +393;31001859;30691 +394;31001859;30690 +395;31000301;30666 +396;31000831;30677 +397;31000831;30671 +398;31001691;30691 +399;31001691;30692 +400;31002247;30702 +401;31001741;30691 +402;31001741;30693 +403;31000885;30687 +404;31002343;30701 +405;31000952;30687 +406;31000107;30667 +407;31000795;30677 +408;31000795;30671 +409;31000621;30678 +410;31000621;30672 +411;31000915;30687 +412;31001367;30687 +413;31001681;30691 +414;31001681;30690 +415;31002199;30702 +416;31000514;30677 +417;31000514;30674 +418;31000947;30687 +419;31001598;30693 +420;31001598;30692 +421;31000259;30666 +422;31001307;30688 +423;31001544;30694 +424;31001544;30692 +425;31001889;30698 +426;31000668;30678 +427;31000668;30672 +428;31001371;30687 +429;31001595;30693 +430;31001595;30692 +431;31001422;30691 +432;31001422;30690 +433;31000773;30677 +434;31000773;30671 +435;31002060;30702 +436;31000184;30666 +437;31000013;30668 +438;31000279;30666 +439;31002009;30702 +440;31000753;30677 +441;31000753;30671 +442;31000296;30666 +443;31002055;30702 +444;31002492;30711 +445;31000267;30666 +446;31001586;30693 +447;31001586;30692 +448;31000199;30666 +449;31002141;30700 +450;31000910;30687 +451;31000292;30666 +452;31000819;30677 +453;31000819;30671 +454;31001490;30690 +455;31001490;30689 +456;31001201;30687 +457;31001592;30693 +458;31001592;30692 +459;31001439;30693 +460;31001439;30694 +461;31002130;30700 +462;31001803;30691 +463;31001803;30693 +464;31001666;30691 +465;31001666;30693 +466;31000622;30678 +467;31000622;30672 +468;31000093;30667 +469;31001777;30691 +470;31001777;30693 +471;31002347;30701 +472;31000416;30677 +473;31000416;30673 +474;31001716;30691 +475;31001716;30689 +476;31001347;30687 +477;31001534;30691 +478;31001534;30692 +479;31001886;30702 +480;31000262;30666 +481;31001092;30695 +482;31002195;30702 +483;31000390;30677 +484;31000390;30673 +485;31000303;30666 +486;31000594;30678 +487;31000594;30672 +488;31001114;30695 +489;31002422;30709 +490;31000052;30667 +491;31000364;30677 +492;31000364;30673 +493;31000432;30677 +494;31000432;30673 +495;31001810;30693 +496;31001810;30692 +497;31002430;30710 +498;31000466;30677 +499;31000466;30674 +500;31002178;30700 +501;31001976;30702 +502;31002442;30711 +503;31000657;30678 +504;31000657;30672 +505;31001186;30687 +506;31000846;30677 +507;31000846;30671 +508;31001796;30691 +509;31001796;30693 +510;31000307;30666 +511;31002078;30703 +512;31000961;30687 +513;31002261;30702 +514;31000864;30677 +515;31000864;30671 +516;31001719;30691 +517;31001719;30689 +518;31000965;30687 +519;31002481;30702 +520;31001491;30690 +521;31001491;30689 +522;31000459;30677 +523;31000459;30674 +524;31001292;30688 +525;31001920;30699 +526;31001382;30690 +527;31001382;30692 +528;31002383;30711 +529;31002294;30701 +530;31000420;30677 +531;31000420;30673 +532;31000661;30678 +533;31000661;30672 +534;31001842;30693 +535;31001842;30689 +536;31000707;30679 +537;31000707;30675 +538;31000426;30677 +539;31000426;30673 +540;31000940;30687 +541;31001939;30698 +542;31001366;30687 +543;31000387;30677 +544;31000387;30673 +545;31000759;30677 +546;31000759;30671 +547;31002447;30711 +548;31000212;30666 +549;31000493;30677 +550;31000493;30674 +551;31002451;30711 +552;31000113;30667 +553;31000066;30667 +554;31000343;30666 +555;31000491;30677 +556;31000491;30674 +557;31001949;30702 +558;31002164;30700 +559;31000362;30677 +560;31000362;30673 +561;31001429;30691 +562;31001429;30690 +563;31000749;30679 +564;31000749;30676 +565;31000503;30677 +566;31000503;30674 +567;31001332;30687 +568;31000231;30666 +569;31001018;30687 +570;31001643;30689 +571;31001643;30692 +572;31001261;30688 +573;31001331;30687 +574;31000201;30666 +575;31001936;30698 +576;31001006;30687 +577;31001556;30693 +578;31001556;30692 +579;31000729;30679 +580;31000729;30675 +581;31000597;30678 +582;31000597;30672 +583;31002152;30700 +584;31001492;30690 +585;31001492;30689 +586;31000691;30678 +587;31000691;30672 +588;31002501;30703 +589;31001645;30691 +590;31001645;30692 +591;31001198;30687 +592;31000163;30666 +593;31002421;30709 +594;31001135;30695 +595;31001453;30693 +596;31001453;30694 +597;31000897;30687 +598;31002291;30701 +599;31001599;30693 +600;31001599;30692 +601;31000630;30678 +602;31000630;30672 +603;31001724;30691 +604;31001724;30692 +605;31001769;30691 +606;31001769;30693 +607;31001379;30690 +608;31001379;30692 +609;31000676;30678 +610;31000676;30672 +611;31002352;30701 +612;31001312;30688 +613;31000544;30677 +614;31000544;30674 +615;31001264;30688 +616;31001116;30695 +617;31001262;30688 +618;31002116;30703 +619;31000572;30678 +620;31000572;30672 +621;31002444;30711 +622;31002367;30708 +623;31001212;30687 +624;31001046;30687 +625;31002394;30709 +626;31001844;30693 +627;31001844;30689 +628;31000547;30677 +629;31000547;30674 +630;31000888;30687 +631;31000891;30687 +632;31000316;30666 +633;31001061;30687 +634;31002437;30711 +635;31001234;30688 +636;31000070;30667 +637;31002214;30702 +638;31000934;30687 +639;31000779;30677 +640;31000779;30671 +641;31002168;30700 +642;31001355;30687 +643;31000502;30677 +644;31000502;30674 +645;31000280;30666 +646;31000800;30677 +647;31000800;30671 +648;31002257;30702 +649;31000807;30677 +650;31000807;30671 +651;31001865;30691 +652;31001865;30692 +653;31000469;30677 +654;31000469;30674 +655;31000732;30679 +656;31000732;30675 +657;31001450;30693 +658;31001450;30694 +659;31001863;30691 +660;31001863;30690 +661;31000563;30678 +662;31000563;30672 +663;31001130;30695 +664;31001495;30689 +665;31001495;30692 +666;31001434;30691 +667;31001434;30690 +668;31001861;30691 +669;31001861;30690 +670;31000701;30678 +671;31000701;30672 +672;31001928;30699 +673;31000007;30668 +674;31001221;30688 +675;31002089;30703 +676;31000044;30667 +677;31002359;30701 +678;31000591;30678 +679;31000591;30672 +680;31001417;30691 +681;31001417;30690 +682;31001346;30687 +683;31002232;30702 +684;31001762;30691 +685;31001762;30690 +686;31002106;30703 +687;31000144;30666 +688;31000299;30666 +689;31000232;30666 +690;31002373;30708 +691;31000017;30668 +692;31002160;30700 +693;31002330;30701 +694;31001455;30694 +695;31001455;30692 +696;31002436;30711 +697;31000470;30677 +698;31000470;30674 +699;31002113;30703 +700;31001740;30691 +701;31001740;30693 +702;31000228;30666 +703;31002068;30703 +704;31001778;30691 +705;31001778;30693 +706;31002487;30712 +707;31000073;30667 +708;31000128;30667 +709;31000368;30677 +710;31000368;30673 +711;31001498;30689 +712;31001498;30692 +713;31002020;30702 +714;31001335;30687 +715;31000429;30677 +716;31000429;30673 +717;31000194;30666 +718;31000156;30666 +719;31000157;30666 +720;31001978;30702 +721;31001197;30687 +722;31000275;30666 +723;31000002;30672 +724;31000130;30667 +725;31001330;30687 +726;31000506;30677 +727;31000506;30674 +728;31000422;30677 +729;31000422;30673 +730;31001211;30687 +731;31000562;30678 +732;31000562;30672 +733;31001735;30691 +734;31001735;30693 +735;31002401;30707 +736;31001170;30695 +737;31000087;30667 +738;31000901;30687 +739;31002245;30702 +740;31000578;30678 +741;31000578;30672 +742;31000168;30666 +743;31001905;30699 +744;31000688;30678 +745;31000688;30672 +746;31000249;30666 +747;31001982;30702 +748;31002268;30702 +749;31000137;30667 +750;31000439;30677 +751;31000439;30673 +752;31000949;30687 +753;31001526;30691 +754;31001526;30692 +755;31001486;30690 +756;31001486;30689 +757;31001314;30688 +758;31000083;30667 +759;31002354;30701 +760;31002417;30709 +761;31000478;30677 +762;31000478;30674 +763;31002404;30710 +764;31002170;30700 +765;31000151;30666 +766;31000645;30678 +767;31000645;30672 +768;31001766;30691 +769;31001766;30693 +770;31001121;30695 +771;31001975;30702 +772;31002356;30701 +773;31001950;30702 +774;31002320;30701 +775;31001662;30691 +776;31001662;30689 +777;31001733;30691 +778;31001733;30692 +779;31000397;30677 +780;31000397;30673 +781;31001411;30693 +782;31001411;30690 +783;31001024;30687 +784;31000879;30677 +785;31000879;30671 +786;31001912;30699 +787;31000690;30678 +788;31000690;30672 +789;31002074;30703 +790;31000731;30679 +791;31000731;30675 +792;31000975;30687 +793;31000889;30687 +794;31001887;30702 +795;31002262;30702 +796;31002491;30702 +797;31002231;30702 +798;31001012;30687 +799;31000020;30668 +800;31001026;30687 +801;31001479;30689 +802;31001479;30692 +803;31000893;30687 +804;31001449;30693 +805;31001449;30694 +806;31001049;30687 +807;31002470;30711 +808;31002382;30711 +809;31001671;30691 +810;31001671;30690 +811;31000406;30677 +812;31000406;30673 +813;31002248;30702 +814;31002407;30710 +815;31002287;30701 +816;31001466;30690 +817;31001466;30689 +818;31000377;30677 +819;31000377;30673 +820;31001051;30687 +821;31002318;30701 +822;31001813;30693 +823;31001813;30692 +824;31000929;30687 +825;31001727;30691 +826;31001727;30692 +827;31000890;30687 +828;31002316;30701 +829;31001713;30691 +830;31001713;30689 +831;31000102;30667 +832;31000366;30677 +833;31000366;30673 +834;31001770;30691 +835;31001770;30693 +836;31001456;30694 +837;31001456;30692 +838;31000923;30687 +839;31000342;30666 +840;31002169;30700 +841;31001308;30688 +842;31000627;30678 +843;31000627;30672 +844;31000946;30687 +845;31002392;30709 +846;31002075;30703 +847;31002325;30701 +848;31001430;30691 +849;31001430;30690 +850;31001973;30702 +851;31000872;30677 +852;31000872;30671 +853;31002406;30710 +854;31002159;30700 +855;31001568;30694 +856;31001568;30692 +857;31000053;30667 +858;31000244;30666 +859;31000413;30677 +860;31000413;30673 +861;31000419;30677 +862;31000419;30673 +863;31001610;30693 +864;31001610;30692 +865;31001667;30691 +866;31001667;30693 +867;31001963;30702 +868;31000190;30666 +869;31000712;30679 +870;31000712;30675 +871;31001903;30699 +872;31001723;30691 +873;31001723;30692 +874;31000340;30666 +875;31002445;30711 +876;31001351;30687 +877;31001641;30689 +878;31001641;30692 +879;31000752;30677 +880;31000752;30671 +881;31000833;30677 +882;31000833;30671 +883;31000765;30677 +884;31000765;30671 +885;31001926;30699 +886;31000921;30687 +887;31001397;30690 +888;31001397;30689 +889;31000538;30677 +890;31000538;30674 +891;31000817;30677 +892;31000817;30671 +893;31000030;30668 +894;31001540;30694 +895;31001540;30692 +896;31001083;30695 +897;31001323;30687 +898;31000797;30677 +899;31000797;30671 +900;31001876;30691 +901;31001876;30692 +902;31000326;30666 +903;31001361;30687 +904;31000438;30677 +905;31000438;30673 +906;31000427;30677 +907;31000427;30673 +908;31002435;30711 +909;31001989;30702 +910;31000914;30687 +911;31001067;30687 +912;31000958;30687 +913;31000143;30666 +914;31000685;30678 +915;31000685;30672 +916;31002315;30701 +917;31001584;30691 +918;31001584;30692 +919;31000345;30666 +920;31001322;30687 +921;31000993;30687 +922;31001293;30688 +923;31001280;30688 +924;31000675;30678 +925;31000675;30672 +926;31000730;30679 +927;31000730;30675 +928;31001189;30687 +929;31000221;30666 +930;31000357;30677 +931;31000357;30673 +932;31002043;30702 +933;31000486;30677 +934;31000486;30674 +935;31001806;30691 +936;31001806;30693 +937;31001864;30691 +938;31001864;30692 +939;31001166;30695 +940;31001789;30693 +941;31001789;30690 +942;31001751;30691 +943;31001751;30693 +944;31000647;30678 +945;31000647;30672 +946;31000869;30677 +947;31000869;30671 +948;31001646;30691 +949;31001646;30692 +950;31000400;30677 +951;31000400;30673 +952;31000985;30687 +953;31001469;30690 +954;31001469;30689 +955;31000674;30678 +956;31000674;30672 +957;31001126;30695 +958;31001205;30687 +959;31001155;30695 +960;31000976;30687 +961;31001203;30687 +962;31000016;30668 +963;31001828;30691 +964;31001828;30693 +965;31000718;30679 +966;31000718;30675 +967;31000924;30687 +968;31001790;30693 +969;31001790;30690 +970;31001408;30693 +971;31001408;30690 +972;31000154;30666 +973;31001746;30691 +974;31001746;30693 +975;31000960;30687 +976;31001029;30687 +977;31001677;30691 +978;31001677;30690 +979;31000972;30687 +980;31000883;30687 +981;31000318;30666 +982;31002045;30702 +983;31001265;30688 +984;31000081;30667 +985;31002018;30702 +986;31001227;30688 +987;31002225;30702 +988;31000069;30667 +989;31000812;30677 +990;31000812;30671 +991;31001516;30689 +992;31001516;30692 +993;31001413;30693 +994;31001413;30690 +995;31001463;30691 +996;31001463;30689 +997;31002026;30702 +998;31001763;30691 +999;31001763;30690 +1000;31000153;30666 +1001;31002101;30703 +1002;31000333;30666 +1003;31001053;30687 +1004;31001377;30690 +1005;31001377;30692 +1006;31001774;30693 +1007;31001774;30692 +1008;31002386;30711 +1009;31000763;30677 +1010;31000763;30671 +1011;31001245;30688 +1012;31002084;30703 +1013;31001985;30702 +1014;31001458;30694 +1015;31001458;30692 +1016;31001834;30693 +1017;31001834;30690 +1018;31000072;30667 +1019;31002193;30700 +1020;31000519;30677 +1021;31000519;30674 +1022;31000905;30687 +1023;31000496;30677 +1024;31000496;30674 +1025;31002281;30702 +1026;31000209;30666 +1027;31001210;30687 +1028;31001052;30687 +1029;31001700;30691 +1030;31001700;30690 +1031;31002003;30702 +1032;31000887;30687 +1033;31000527;30677 +1034;31000527;30674 +1035;31000766;30677 +1036;31000766;30671 +1037;31000214;30666 +1038;31002302;30701 +1039;31001826;30691 +1040;31001826;30693 +1041;31000970;30687 +1042;31002050;30702 +1043;31000881;30687 +1044;31000380;30677 +1045;31000380;30673 +1046;31001940;30698 +1047;31001532;30691 +1048;31001532;30692 +1049;31001161;30695 +1050;31002006;30702 +1051;31000482;30677 +1052;31000482;30674 +1053;31001721;30691 +1054;31001721;30692 +1055;31000189;30666 +1056;31002441;30711 +1057;31002198;30702 +1058;31002057;30702 +1059;31001231;30688 +1060;31001164;30695 +1061;31002059;30702 +1062;31000593;30678 +1063;31000593;30672 +1064;31001679;30691 +1065;31001679;30690 +1066;31002182;30700 +1067;31000115;30667 +1068;31000164;30666 +1069;31001267;30688 +1070;31001033;30687 +1071;31000899;30687 +1072;31000011;30668 +1073;31000550;30677 +1074;31000550;30674 +1075;31000216;30666 +1076;31001707;30691 +1077;31001707;30693 +1078;31000589;30678 +1079;31000589;30672 +1080;31000494;30677 +1081;31000494;30674 +1082;31002503;30702 +1083;31000374;30677 +1084;31000374;30673 +1085;31002088;30703 +1086;31000120;30667 +1087;31000287;30666 +1088;31001147;30695 +1089;31002434;30710 +1090;31001015;30687 +1091;31001387;30690 +1092;31001387;30692 +1093;31001513;30689 +1094;31001513;30692 +1095;31001824;30693 +1096;31001824;30692 +1097;31002013;30702 +1098;31000131;30667 +1099;31001459;30694 +1100;31001459;30692 +1101;31002312;30701 +1102;31002004;30702 +1103;31001066;30687 +1104;31000205;30666 +1105;31000283;30666 +1106;31000820;30677 +1107;31000820;30671 +1108;31000994;30687 +1109;31001609;30693 +1110;31001609;30692 +1111;31002165;30700 +1112;31000694;30678 +1113;31000694;30672 +1114;31000613;30678 +1115;31000613;30672 +1116;31000750;30679 +1117;31000750;30676 +1118;31000588;30678 +1119;31000588;30672 +1120;31000686;30678 +1121;31000686;30672 +1122;31002238;30702 +1123;31002142;30700 +1124;31002177;30700 +1125;31001358;30687 +1126;31001149;30695 +1127;31001045;30687 +1128;31001242;30688 +1129;31001969;30702 +1130;31002134;30700 +1131;31002064;30703 +1132;31001634;30690 +1133;31001634;30692 +1134;31000624;30678 +1135;31000624;30672 +1136;31000119;30667 +1137;31001404;30693 +1138;31001404;30690 +1139;31000064;30667 +1140;31001481;30689 +1141;31001481;30692 +1142;31001672;30691 +1143;31001672;30690 +1144;31002194;30702 +1145;31000475;30677 +1146;31000475;30674 +1147;31001693;30691 +1148;31001693;30692 +1149;31000734;30679 +1150;31000734;30675 +1151;31000906;30687 +1152;31002311;30701 +1153;31000611;30678 +1154;31000611;30672 +1155;31000192;30666 +1156;31000346;30666 +1157;31000160;30666 +1158;31000325;30666 +1159;31002375;30712 +1160;31002377;30712 +1161;31002280;30702 +1162;31002299;30701 +1163;31002243;30702 +1164;31001374;30687 +1165;31000585;30678 +1166;31000585;30672 +1167;31000918;30687 +1168;31002371;30708 +1169;31002136;30700 +1170;31002396;30709 +1171;31002252;30702 +1172;31002107;30703 +1173;31002419;30709 +1174;31001258;30688 +1175;31001848;30693 +1176;31001848;30694 +1177;31001849;30693 +1178;31001849;30694 +1179;31000603;30678 +1180;31000603;30672 +1181;31000289;30666 +1182;31000425;30677 +1183;31000425;30673 +1184;31000446;30677 +1185;31000446;30673 +1186;31001835;30693 +1187;31001835;30690 +1188;31000200;30666 +1189;31000273;30666 +1190;31002292;30701 +1191;31001142;30695 +1192;31001551;30693 +1193;31001551;30692 +1194;31000306;30666 +1195;31002131;30700 +1196;31000546;30677 +1197;31000546;30674 +1198;31001554;30693 +1199;31001554;30692 +1200;31001022;30687 +1201;31000722;30679 +1202;31000722;30675 +1203;31002389;30709 +1204;31000121;30667 +1205;31002213;30702 +1206;31000497;30677 +1207;31000497;30674 +1208;31000226;30666 +1209;31000794;30677 +1210;31000794;30671 +1211;31001505;30691 +1212;31001505;30689 +1213;31000629;30678 +1214;31000629;30672 +1215;31000370;30677 +1216;31000370;30673 +1217;31000150;30666 +1218;31001345;30687 +1219;31002500;30698 +1220;31001990;30702 +1221;31002092;30703 +1222;31001845;30693 +1223;31001845;30689 +1224;31002322;30701 +1225;31000774;30677 +1226;31000774;30671 +1227;31000500;30677 +1228;31000500;30674 +1229;31001393;30690 +1230;31001393;30692 +1231;31001059;30687 +1232;31001712;30691 +1233;31001712;30693 +1234;31001362;30687 +1235;31001141;30695 +1236;31001593;30693 +1237;31001593;30692 +1238;31000080;30667 +1239;31001958;30702 +1240;31000220;30666 +1241;31001910;30699 +1242;31001150;30695 +1243;31000813;30677 +1244;31000813;30671 +1245;31001501;30691 +1246;31001501;30689 +1247;31000022;30668 +1248;31000554;30677 +1249;31000554;30674 +1250;31002118;30703 +1251;31000152;30666 +1252;31001290;30688 +1253;31002204;30702 +1254;31000989;30687 +1255;31000430;30677 +1256;31000430;30673 +1257;31002197;30702 +1258;31001295;30688 +1259;31000114;30667 +1260;31001642;30689 +1261;31001642;30692 +1262;31001175;30695 +1263;31001528;30691 +1264;31001528;30692 +1265;31001013;30687 +1266;31001959;30702 +1267;31000658;30678 +1268;31000658;30672 +1269;31001349;30687 +1270;31002066;30703 +1271;31001195;30687 +1272;31000861;30677 +1273;31000861;30671 +1274;31000733;30679 +1275;31000733;30675 +1276;31000703;30679 +1277;31000703;30675 +1278;31002223;30702 +1279;31000828;30677 +1280;31000828;30671 +1281;31002051;30702 +1282;31001407;30693 +1283;31001407;30690 +1284;31001703;30691 +1285;31001703;30693 +1286;31001073;30695 +1287;31000347;30666 +1288;31001224;30688 +1289;31000395;30677 +1290;31000395;30673 +1291;31000700;30678 +1292;31000700;30672 +1293;31000315;30666 +1294;31000327;30666 +1295;31000699;30678 +1296;31000699;30672 +1297;31002067;30703 +1298;31001206;30687 +1299;31002062;30703 +1300;31001838;30693 +1301;31001838;30690 +1302;31000252;30666 +1303;31001944;30702 +1304;31000799;30677 +1305;31000799;30671 +1306;31000537;30677 +1307;31000537;30674 +1308;31000757;30677 +1309;31000757;30671 +1310;31001497;30689 +1311;31001497;30692 +1312;31001027;30687 +1313;31001122;30695 +1314;31000350;30666 +1315;31000522;30677 +1316;31000522;30674 +1317;31002424;30709 +1318;31001162;30695 +1319;31000507;30677 +1320;31000507;30674 +1321;31001244;30688 +1322;31002453;30711 +1323;31001802;30691 +1324;31001802;30693 +1325;31001882;30702 +1326;31002306;30701 +1327;31002218;30702 +1328;31000736;30679 +1329;31000736;30675 +1330;31001531;30691 +1331;31001531;30692 +1332;31002069;30703 +1333;31000780;30677 +1334;31000780;30671 +1335;31000723;30679 +1336;31000723;30675 +1337;31001445;30691 +1338;31001445;30694 +1339;31000147;30666 +1340;31001825;30693 +1341;31001825;30692 +1342;31000255;30666 +1343;31000202;30666 +1344;31001918;30702 +1345;31001765;30691 +1346;31001765;30693 +1347;31000823;30677 +1348;31000823;30671 +1349;31002145;30700 +1350;31000984;30687 +1351;31001340;30687 +1352;31002449;30711 +1353;31000394;30677 +1354;31000394;30673 +1355;31000472;30677 +1356;31000472;30674 +1357;31000664;30678 +1358;31000664;30672 +1359;31000250;30666 +1360;31001075;30695 +1361;31000785;30677 +1362;31000785;30671 +1363;31001573;30691 +1364;31001573;30692 +1365;31000134;30667 +1366;31002341;30701 +1367;31000133;30667 +1368;31000907;30687 +1369;31002150;30700 +1370;31001870;30691 +1371;31001870;30692 +1372;31000693;30678 +1373;31000693;30672 +1374;31000909;30687 +1375;31001761;30691 +1376;31001761;30690 +1377;31001682;30691 +1378;31001682;30690 +1379;31001031;30687 +1380;31000711;30679 +1381;31000711;30675 +1382;31002270;30702 +1383;31000574;30678 +1384;31000574;30672 +1385;31002117;30703 +1386;31001289;30688 +1387;31000488;30677 +1388;31000488;30674 +1389;31000951;30687 +1390;31000082;30667 +1391;31001577;30691 +1392;31001577;30692 +1393;31001418;30691 +1394;31001418;30690 +1395;31000796;30677 +1396;31000796;30671 +1397;31002331;30701 +1398;31000709;30679 +1399;31000709;30675 +1400;31002015;30702 +1401;31002097;30703 +1402;31002203;30702 +1403;31000096;30667 +1404;31001398;30690 +1405;31001398;30689 +1406;31001725;30691 +1407;31001725;30692 +1408;31000241;30666 +1409;31001654;30691 +1410;31001654;30692 +1411;31000320;30666 +1412;31000097;30667 +1413;31001054;30687 +1414;31000713;30679 +1415;31000713;30675 +1416;31000605;30678 +1417;31000605;30672 +1418;31002443;30711 +1419;31001694;30691 +1420;31001694;30692 +1421;31000801;30677 +1422;31000801;30671 +1423;31000737;30679 +1424;31000737;30675 +1425;31000451;30677 +1426;31000451;30673 +1427;31000725;30679 +1428;31000725;30675 +1429;31002008;30702 +1430;31000242;30666 +1431;31002269;30702 +1432;31002017;30702 +1433;31001576;30691 +1434;31001576;30692 +1435;31000990;30687 +1436;31002209;30702 +1437;31002459;30711 +1438;31000920;30687 +1439;31001336;30687 +1440;31000265;30666 +1441;31000378;30677 +1442;31000378;30673 +1443;31001775;30693 +1444;31001775;30692 +1445;31001675;30691 +1446;31001675;30690 +1447;31000848;30677 +1448;31000848;30671 +1449;31001041;30687 +1450;31001601;30690 +1451;31001601;30692 +1452;31001152;30695 +1453;31000898;30687 +1454;31001350;30687 +1455;31001992;30702 +1456;31000851;30677 +1457;31000851;30671 +1458;31000295;30666 +1459;31002411;30712 +1460;31000359;30677 +1461;31000359;30673 +1462;31000788;30677 +1463;31000788;30671 +1464;31000616;30678 +1465;31000616;30672 +1466;31002187;30700 +1467;31000866;30677 +1468;31000866;30671 +1469;31000935;30687 +1470;31001313;30688 +1471;31000912;30687 +1472;31001069;30687 +1473;31001057;30687 +1474;31002221;30702 +1475;31001558;30693 +1476;31001558;30692 +1477;31001894;30698 +1478;31001993;30702 +1479;31001363;30687 +1480;31000756;30677 +1481;31000756;30671 +1482;31000886;30687 +1483;31001058;30687 +1484;31001089;30695 +1485;31001399;30690 +1486;31001399;30689 +1487;31000054;30667 +1488;31001603;30690 +1489;31001603;30692 +1490;31001625;30691 +1491;31001625;30692 +1492;31002174;30700 +1493;31001929;30699 +1494;31000127;30667 +1495;31001297;30688 +1496;31001943;30702 +1497;31000863;30677 +1498;31000863;30671 +1499;31001485;30689 +1500;31001485;30692 +1501;31001055;30687 +1502;31002276;30702 +1503;31002460;30711 +1504;31002502;30711 +1505;31001047;30687 +1506;31000140;30666 +1507;31001549;30693 +1508;31001549;30692 +1509;31001165;30695 +1510;31000356;30677 +1511;31000356;30673 +1512;31001163;30695 +1513;31002497;30701 +1514;31001507;30690 +1515;31001507;30692 +1516;31002128;30700 +1517;31001462;30691 +1518;31001462;30689 +1519;31001805;30691 +1520;31001805;30693 +1521;31000874;30677 +1522;31000874;30671 +1523;31001127;30695 +1524;31000035;30667 +1525;31001287;30688 +1526;31002335;30701 +1527;31002149;30700 +1528;31001869;30691 +1529;31001869;30692 +1530;31002244;30702 +1531;31002188;30700 +1532;31000328;30666 +1533;31002504;30700 +1534;31000043;30667 +1535;31002296;30701 +1536;31000511;30677 +1537;31000511;30674 +1538;31000665;30678 +1539;31000665;30672 +1540;31001032;30687 +1541;31000745;30679 +1542;31000745;30676 +1543;31001005;30687 +1544;31000061;30667 +1545;31001955;30702 +1546;31000108;30667 +1547;31000743;30679 +1548;31000743;30676 +1549;31000186;30666 +1550;31000565;30678 +1551;31000565;30672 +1552;31001657;30691 +1553;31001657;30689 +1554;31000982;30687 +1555;31001240;30688 +1556;31000955;30687 +1557;31000367;30677 +1558;31000367;30673 +1559;31002058;30702 +1560;31001816;30693 +1561;31001816;30690 +1562;31001352;30687 +1563;31001924;30699 +1564;31002022;30702 +1565;31002260;30702 +1566;31000240;30666 +1567;31001025;30687 +1568;31000767;30677 +1569;31000767;30671 +1570;31000268;30666 +1571;31001525;30691 +1572;31001525;30692 +1573;31000471;30677 +1574;31000471;30674 +1575;31000467;30677 +1576;31000467;30674 +1577;31001843;30693 +1578;31001843;30689 +1579;31002029;30702 +1580;31001098;30695 +1581;31001392;30690 +1582;31001392;30692 +1583;31000399;30677 +1584;31000399;30673 +1585;31002090;30703 +1586;31002286;30701 +1587;31000217;30666 +1588;31001783;30693 +1589;31001783;30692 +1590;31001530;30691 +1591;31001530;30692 +1592;31002180;30700 +1593;31001582;30691 +1594;31001582;30692 +1595;31002028;30702 +1596;31000963;30687 +1597;31001750;30691 +1598;31001750;30693 +1599;31002206;30702 +1600;31001380;30690 +1601;31001380;30692 +1602;31001011;30687 +1603;31000215;30666 +1604;31001620;30690 +1605;31001620;30692 +1606;31000095;30667 +1607;31001343;30687 +1608;31002098;30703 +1609;31000566;30678 +1610;31000566;30672 +1611;31000706;30679 +1612;31000706;30675 +1613;31001930;30699 +1614;31000058;30667 +1615;31001207;30687 +1616;31001961;30702 +1617;31001268;30688 +1618;31001701;30691 +1619;31001701;30693 +1620;31002488;30702 +1621;31001572;30694 +1622;31001572;30692 +1623;31000716;30679 +1624;31000716;30675 +1625;31001078;30695 +1626;31000050;30667 +1627;31000504;30677 +1628;31000504;30674 +1629;31002380;30711 +1630;31000148;30666 +1631;31001517;30689 +1632;31001517;30692 +1633;31001538;30691 +1634;31001538;30692 +1635;31001074;30695 +1636;31000672;30678 +1637;31000672;30672 +1638;31001589;30693 +1639;31001589;30692 +1640;31001427;30693 +1641;31001427;30690 +1642;31000485;30677 +1643;31000485;30674 +1644;31001624;30690 +1645;31001624;30692 +1646;31000573;30678 +1647;31000573;30672 +1648;31002080;30703 +1649;31002278;30702 +1650;31000442;30677 +1651;31000442;30673 +1652;31001527;30691 +1653;31001527;30692 +1654;31000480;30677 +1655;31000480;30674 +1656;31000900;30687 +1657;31000172;30666 +1658;31000545;30677 +1659;31000545;30674 +1660;31001237;30688 +1661;31001611;30693 +1662;31001611;30692 +1663;31001369;30687 +1664;31000762;30677 +1665;31000762;30671 +1666;31001270;30688 +1667;31001965;30702 +1668;31002275;30702 +1669;31002237;30702 +1670;31002337;30701 +1671;31002321;30701 +1672;31002138;30700 +1673;31000204;30666 +1674;31001467;30690 +1675;31001467;30689 +1676;31001133;30695 +1677;31000697;30678 +1678;31000697;30672 +1679;31001878;30691 +1680;31001878;30689 +1681;31002201;30702 +1682;31001255;30688 +1683;31000351;30666 +1684;31001722;30691 +1685;31001722;30692 +1686;31000654;30678 +1687;31000654;30672 +1688;31001546;30694 +1689;31001546;30692 +1690;31001219;30687 +1691;31002041;30702 +1692;31000742;30679 +1693;31000742;30676 +1694;31002175;30700 +1695;31000646;30678 +1696;31000646;30672 +1697;31000944;30687 +1698;31001100;30695 +1699;31000631;30678 +1700;31000631;30672 +1701;31000839;30677 +1702;31000839;30671 +1703;31000810;30677 +1704;31000810;30671 +1705;31000036;30667 +1706;31001109;30695 +1707;31002418;30709 +1708;31001931;30699 +1709;31000391;30677 +1710;31000391;30673 +1711;31000830;30677 +1712;31000830;30671 +1713;31000530;30677 +1714;31000530;30674 +1715;31001228;30688 +1716;31000634;30678 +1717;31000634;30672 +1718;31000518;30677 +1719;31000518;30674 +1720;31000860;30677 +1721;31000860;30671 +1722;31002466;30711 +1723;31001737;30691 +1724;31001737;30693 +1725;31001476;30693 +1726;31001476;30689 +1727;31001873;30691 +1728;31001873;30692 +1729;31000196;30666 +1730;31001076;30695 +1731;31001299;30688 +1732;31001536;30691 +1733;31001536;30692 +1734;31001953;30702 +1735;31001472;30693 +1736;31001472;30689 +1737;31002473;30701 +1738;31000598;30678 +1739;31000598;30672 +1740;31000434;30677 +1741;31000434;30673 +1742;31002049;30702 +1743;31000618;30678 +1744;31000618;30672 +1745;31000964;30687 +1746;31001833;30693 +1747;31001833;30690 +1748;31002304;30701 +1749;31000401;30677 +1750;31000401;30673 +1751;31000523;30677 +1752;31000523;30674 +1753;31001952;30702 +1754;31002242;30702 +1755;31001420;30691 +1756;31001420;30690 +1757;31000596;30678 +1758;31000596;30672 +1759;31000969;30687 +1760;31000615;30678 +1761;31000615;30672 +1762;31001113;30695 +1763;31001758;30691 +1764;31001758;30694 +1765;31001097;30695 +1766;31001784;30693 +1767;31001784;30692 +1768;31001259;30688 +1769;31001070;30687 +1770;31001431;30691 +1771;31001431;30690 +1772;31000197;30666 +1773;31001932;30699 +1774;31001895;30698 +1775;31001337;30687 +1776;31000501;30677 +1777;31000501;30674 +1778;31002229;30702 +1779;31002496;30702 +1780;31001731;30691 +1781;31001731;30692 +1782;31000557;30677 +1783;31000557;30674 +1784;31002362;30701 +1785;31000091;30667 +1786;31001094;30695 +1787;31000836;30677 +1788;31000836;30671 +1789;31000302;30666 +1790;31000625;30678 +1791;31000625;30672 +1792;31001444;30691 +1793;31001444;30694 +1794;31001893;30698 +1795;31001171;30695 +1796;31000281;30666 +1797;31000853;30677 +1798;31000853;30671 +1799;31001579;30691 +1800;31001579;30692 +1801;31001088;30695 +1802;31000365;30677 +1803;31000365;30673 +1804;31001356;30687 +1805;31002112;30703 +1806;31001004;30687 +1807;31002457;30711 +1808;31000332;30666 +1809;31001341;30687 +1810;31002010;30702 +1811;31000348;30666 +1812;31000185;30666 +1813;31001585;30691 +1814;31001585;30692 +1815;31000755;30677 +1816;31000755;30671 +1817;31000508;30677 +1818;31000508;30674 +1819;31001919;30702 +1820;31001388;30690 +1821;31001388;30692 +1822;31002290;30701 +1823;31001215;30687 +1824;31001971;30702 +1825;31002137;30700 +1826;31001901;30699 +1827;31002176;30700 +1828;31000560;30677 +1829;31000560;30674 +1830;31000174;30666 +1831;31000741;30679 +1832;31000741;30675 +1833;31001437;30693 +1834;31001437;30694 +1835;31000950;30687 +1836;31000161;30666 +1837;31002104;30703 +1838;31001080;30695 +1839;31002376;30712 +1840;31002123;30703 +1841;31002336;30701 +1842;31001785;30693 +1843;31001785;30692 +1844;31001612;30690 +1845;31001612;30692 +1846;31001281;30688 +1847;31001608;30693 +1848;31001608;30692 +1849;31000375;30677 +1850;31000375;30673 +1851;31000986;30687 +1852;31002171;30700 +1853;31001957;30702 +1854;31000942;30687 +1855;31000571;30678 +1856;31000571;30672 +1857;31001272;30688 +1858;31001079;30695 +1859;31000126;30667 +1860;31001125;30695 +1861;31002087;30703 +1862;31000099;30667 +1863;31000198;30666 +1864;31001847;30693 +1865;31001847;30694 +1866;31001885;30702 +1867;31001017;30687 +1868;31000838;30677 +1869;31000838;30671 +1870;31000014;30668 +1871;31000476;30677 +1872;31000476;30674 +1873;31000178;30666 +1874;31001743;30691 +1875;31001743;30693 +1876;31000643;30678 +1877;31000643;30672 +1878;31001687;30691 +1879;31001687;30692 +1880;31000747;30679 +1881;31000747;30676 +1882;31000790;30677 +1883;31000790;30671 +1884;31001415;30693 +1885;31001415;30690 +1886;31001623;30690 +1887;31001623;30692 +1888;31001140;30695 +1889;31002183;30700 +1890;31000088;30667 +1891;31001877;30691 +1892;31001877;30689 +1893;31001578;30691 +1894;31001578;30692 +1895;31000558;30677 +1896;31000558;30674 +1897;31000294;30666 +1898;31000312;30666 +1899;31000516;30677 +1900;31000516;30674 +1901;31001204;30687 +1902;31000125;30667 +1903;31002096;30703 +1904;31000437;30677 +1905;31000437;30673 +1906;31002230;30702 +1907;31001446;30691 +1908;31001446;30694 +1909;31000590;30678 +1910;31000590;30672 +1911;31001587;30693 +1912;31001587;30692 +1913;31000726;30679 +1914;31000726;30675 +1915;31000405;30677 +1916;31000405;30673 +1917;31000330;30666 +1918;31001511;30690 +1919;31001511;30692 +1920;31000012;30668 +1921;31000109;30667 +1922;31000617;30678 +1923;31000617;30672 +1924;31000440;30677 +1925;31000440;30673 +1926;31002463;30711 +1927;31001139;30695 +1928;31000021;30668 +1929;31001987;30702 +1930;31001881;30702 +1931;31001373;30687 +1932;31000680;30678 +1933;31000680;30672 +1934;31000586;30678 +1935;31000586;30672 +1936;31002122;30703 +1937;31001218;30687 +1938;31002440;30711 +1939;31000878;30677 +1940;31000878;30671 +1941;31000103;30667 +1942;31000101;30667 +1943;31002127;30700 +1944;31001184;30687 +1945;31002349;30701 +1946;31001659;30691 +1947;31001659;30689 +1948;31001008;30687 +1949;31002207;30702 +1950;31001256;30688 +1951;31000042;30667 +1952;31000792;30677 +1953;31000792;30671 +1954;31001900;30699 +1955;31002370;30708 +1956;31001489;30690 +1957;31001489;30689 +1958;31000977;30687 +1959;31002313;30701 +1960;31002047;30702 +1961;31001757;30691 +1962;31001757;30694 +1963;31002284;30701 +1964;31001960;30702 +1965;31002391;30709 +1966;31002298;30701 +1967;31000623;30678 +1968;31000623;30672 +1969;31001359;30687 +1970;31002228;30702 +1971;31000513;30677 +1972;31000513;30674 +1973;31002346;30701 +1974;31001354;30687 +1975;31001764;30691 +1976;31001764;30690 +1977;31000489;30677 +1978;31000489;30674 +1979;31002477;30701 +1980;31000653;30678 +1981;31000653;30672 +1982;31001523;30691 +1983;31001523;30692 +1984;31001342;30687 +1985;31001602;30690 +1986;31001602;30692 +1987;31000529;30677 +1988;31000529;30674 +1989;31000455;30677 +1990;31000455;30673 +1991;31000248;30666 +1992;31000943;30687 +1993;31001792;30693 +1994;31001792;30690 +1995;31000510;30677 +1996;31000510;30674 +1997;31000074;30667 +1998;31002358;30701 +1999;31000282;30666 +2000;31001728;30691 +2001;31001728;30692 +2002;31002454;30711 +2003;31001618;30690 +2004;31001618;30692 +2005;31001547;30693 +2006;31001547;30692 +2007;31001696;30691 +2008;31001696;30690 +2009;31002215;30702 +2010;31000525;30677 +2011;31000525;30674 +2012;31001190;30687 +2013;31002033;30702 +2014;31000945;30687 +2015;31002301;30701 +2016;31000758;30677 +2017;31000758;30671 +2018;31002166;30700 +2019;31002154;30700 +2020;31000930;30687 +2021;31002234;30702 +2022;31001552;30693 +2023;31001552;30692 +2024;31002489;30712 +2025;31002011;30702 +2026;31000138;30667 +2027;31001934;30698 +2028;31000526;30677 +2029;31000526;30674 +2030;31001922;30699 +2031;31001854;30691 +2032;31001854;30692 +2033;31000815;30677 +2034;31000815;30671 +2035;31001325;30687 +2036;31000065;30667 +2037;31000728;30679 +2038;31000728;30675 +2039;31000931;30687 +2040;31001705;30691 +2041;31001705;30693 +2042;31000638;30678 +2043;31000638;30672 +2044;31001771;30693 +2045;31001771;30692 +2046;31000141;30666 +2047;31000825;30677 +2048;31000825;30671 +2049;31001569;30694 +2050;31001569;30692 +2051;31002429;30710 +2052;31001742;30691 +2053;31001742;30693 +2054;31001664;30691 +2055;31001664;30693 +2056;31001036;30687 +2057;31000925;30687 +2058;31000639;30678 +2059;31000639;30672 +2060;31000239;30666 +2061;31000106;30667 +2062;31001630;30691 +2063;31001630;30692 +2064;31002348;30701 +2065;31000263;30666 +2066;31002120;30703 +2067;31001247;30688 +2068;31001405;30693 +2069;31001405;30690 +2070;31001706;30691 +2071;31001706;30693 +2072;31001890;30698 +2073;31000235;30666 +2074;31000360;30677 +2075;31000360;30673 +2076;31001964;30702 +2077;31001424;30693 +2078;31001424;30690 +2079;31001640;30689 +2080;31001640;30692 +2081;31000118;30667 +2082;31002105;30703 +2083;31001567;30694 +2084;31001567;30692 +2085;31001788;30693 +2086;31001788;30692 +2087;31002467;30711 +2088;31001521;30691 +2089;31001521;30692 +2090;31000462;30677 +2091;31000462;30674 +2092;31001580;30691 +2093;31001580;30692 +2094;31001702;30691 +2095;31001702;30693 +2096;31002374;30712 +2097;31000704;30679 +2098;31000704;30675 +2099;31001108;30695 +2100;31002433;30710 +2101;31000222;30666 +2102;31001956;30702 +2103;31000811;30677 +2104;31000811;30671 +2105;31002111;30703 +2106;31001561;30693 +2107;31001561;30692 +2108;31000816;30677 +2109;31000816;30671 +2110;31001858;30691 +2111;31001858;30690 +2112;31002189;30700 +2113;31001759;30691 +2114;31001759;30690 +2115;31000352;30666 +2116;31000124;30667 +2117;31001065;30687 +2118;31001365;30687 +2119;31000369;30677 +2120;31000369;30673 +2121;31001539;30691 +2122;31001539;30692 +2123;31000933;30687 +2124;31001023;30687 +2125;31000962;30687 +2126;31000679;30678 +2127;31000679;30672 +2128;31002158;30700 +2129;31000304;30666 +2130;31001734;30691 +2131;31001734;30693 +2132;31000298;30666 +2133;31000293;30666 +2134;31002323;30701 +2135;31001381;30690 +2136;31001381;30692 +2137;31001441;30691 +2138;31001441;30694 +2139;31002044;30702 +2140;31000532;30677 +2141;31000532;30674 +2142;31001426;30693 +2143;31001426;30690 +2144;31002405;30710 +2145;31000411;30677 +2146;31000411;30673 +2147;31000353;30666 +2148;31001739;30691 +2149;31001739;30693 +2150;31002172;30700 +2151;31001500;30691 +2152;31001500;30689 +2153;31001148;30695 +2154;31000916;30687 +2155;31001983;30702 +2156;31001158;30695 +2157;31001820;30693 +2158;31001820;30692 +2159;31001000;30687 +2160;31002379;30712 +2161;31000418;30677 +2162;31000418;30673 +2163;31002366;30701 +2164;31000410;30677 +2165;31000410;30673 +2166;31001179;30687 +2167;31000187;30666 +2168;31002037;30702 +2169;31002095;30703 +2170;31000270;30666 +2171;31002303;30701 +2172;31000059;30667 +2173;31001748;30691 +2174;31001748;30693 +2175;31002475;30701 +2176;31001594;30693 +2177;31001594;30692 +2178;31000567;30678 +2179;31000567;30672 +2180;31000671;30678 +2181;31000671;30672 +2182;31000727;30679 +2183;31000727;30675 +2184;31000666;30678 +2185;31000666;30672 +2186;31002046;30702 +2187;31002277;30702 +2188;31000599;30678 +2189;31000599;30672 +2190;31001457;30694 +2191;31001457;30692 +2192;31000721;30679 +2193;31000721;30675 +2194;31000274;30666 +2195;31001102;30695 +2196;31000256;30666 +2197;31001214;30687 +2198;31002053;30702 +2199;31002081;30703 +2200;31001196;30687 +2201;31001942;30698 +2202;31002196;30702 +2203;31000229;30666 +2204;31001613;30690 +2205;31001613;30692 +2206;31002403;30710 +2207;31000089;30667 +2208;31000032;30668 +2209;31000735;30679 +2210;31000735;30675 +2211;31002220;30702 +2212;31000650;30678 +2213;31000650;30672 +2214;31000579;30678 +2215;31000579;30672 +2216;31001357;30687 +2217;31000473;30677 +2218;31000473;30674 +2219;31000290;30666 +2220;31000436;30677 +2221;31000436;30673 +2222;31000932;30687 +2223;31000689;30678 +2224;31000689;30672 +2225;31000417;30677 +2226;31000417;30673 +2227;31001082;30695 +2228;31000331;30666 +2229;31002295;30701 +2230;31000715;30679 +2231;31000715;30675 +2232;31001815;30693 +2233;31001815;30690 +2234;31002499;30700 +2235;31001317;30687 +2236;31002156;30700 +2237;31000234;30666 +2238;31000407;30677 +2239;31000407;30673 +2240;31000246;30666 +2241;31000695;30678 +2242;31000695;30672 +2243;31001316;30688 +2244;31000659;30678 +2245;31000659;30672 +2246;31002086;30703 +2247;31001968;30702 +2248;31002387;30711 +2249;31002309;30701 +2250;31001468;30690 +2251;31001468;30689 +2252;31002297;30701 +2253;31001786;30693 +2254;31001786;30692 +2255;31000937;30687 +2256;31001134;30695 +2257;31001635;30690 +2258;31001635;30692 +2259;31000739;30679 +2260;31000739;30675 +2261;31001506;30690 +2262;31001506;30692 +2263;31000784;30677 +2264;31000784;30671 +2265;31001339;30687 +2266;31001807;30693 +2267;31001807;30692 +2268;31000606;30678 +2269;31000606;30672 +2270;31001222;30688 +2271;31002032;30702 +2272;31001962;30702 +2273;31001660;30691 +2274;31001660;30689 +2275;31001710;30691 +2276;31001710;30693 +2277;31001698;30691 +2278;31001698;30690 +2279;31000363;30677 +2280;31000363;30673 +2281;31000824;30677 +2282;31000824;30671 +2283;31000373;30677 +2284;31000373;30673 +2285;31000277;30666 +2286;31000079;30667 +2287;31001266;30688 +2288;31000850;30677 +2289;31000850;30671 +2290;31001648;30691 +2291;31001648;30692 +2292;31001533;30691 +2293;31001533;30692 +2294;31001019;30687 +2295;31002384;30711 +2296;31000814;30677 +2297;31000814;30671 +2298;31000535;30677 +2299;31000535;30674 +2300;31002264;30702 +2301;31002414;30712 +2302;31000959;30687 +2303;31000117;30667 +2304;31002353;30701 +2305;31001423;30693 +2306;31001423;30690 +2307;31000041;30667 +2308;31002202;30702 +2309;31000464;30677 +2310;31000464;30674 +2311;31001110;30695 +2312;31000372;30677 +2313;31000372;30673 +2314;31000902;30687 +2315;31000183;30666 +2316;31001154;30695 +2317;31000999;30687 +2318;31000159;30666 +2319;31001090;30695 +2320;31000649;30678 +2321;31000649;30672 +2322;31001508;30690 +2323;31001508;30692 +2324;31000772;30677 +2325;31000772;30671 +2326;31001729;30691 +2327;31001729;30692 +2328;31001409;30693 +2329;31001409;30690 +2330;31001182;30687 +2331;31000354;30666 +2332;31000447;30677 +2333;31000447;30673 +2334;31001791;30693 +2335;31001791;30690 +2336;31000038;30667 +2337;31002448;30711 +2338;31000040;30667 +2339;31000056;30667 +2340;31002258;30702 +2341;31000549;30677 +2342;31000549;30674 +2343;31001038;30687 +2344;31001412;30693 +2345;31001412;30690 +2346;31002319;30701 +2347;31000821;30677 +2348;31000821;30671 +2349;31000481;30677 +2350;31000481;30674 +2351;31001911;30699 +2352;31002485;30702 +2353;31000555;30677 +2354;31000555;30674 +2355;31000581;30678 +2356;31000581;30672 +2357;31001518;30689 +2358;31001518;30692 +2359;31000787;30677 +2360;31000787;30671 +2361;31002327;30701 +2362;31002125;30703 +2363;31000539;30677 +2364;31000539;30674 +2365;31001419;30691 +2366;31001419;30690 +2367;31002129;30700 +2368;31001988;30702 +2369;31001874;30691 +2370;31001874;30692 +2371;31000048;30667 +2372;31002249;30702 +2373;31002071;30703 +2374;31000051;30667 +2375;31002147;30700 +2376;31001840;30693 +2377;31001840;30689 +2378;31000822;30677 +2379;31000822;30671 +2380;31000868;30677 +2381;31000868;30671 +2382;31001512;30690 +2383;31001512;30692 +2384;31002416;30712 +2385;31002103;30703 +2386;31002498;30711 +2387;31001629;30691 +2388;31001629;30692 +2389;31001278;30688 +2390;31001562;30693 +2391;31001562;30692 +2392;31001111;30695 +2393;31000636;30678 +2394;31000636;30672 +2395;31001370;30687 +2396;31000856;30677 +2397;31000856;30671 +2398;31001106;30695 +2399;31001564;30693 +2400;31001564;30692 +2401;31002272;30702 +2402;31000388;30677 +2403;31000388;30673 +2404;31001226;30688 +2405;31002077;30703 +2406;31001720;30691 +2407;31001720;30692 +2408;31001755;30691 +2409;31001755;30694 +2410;31002205;30702 +2411;31001581;30691 +2412;31001581;30692 +2413;31000465;30677 +2414;31000465;30674 +2415;31000384;30677 +2416;31000384;30673 +2417;31002241;30702 +2418;31001484;30689 +2419;31001484;30692 +2420;31002065;30703 +2421;31002250;30702 +2422;31001652;30691 +2423;31001652;30692 +2424;31001096;30695 +2425;31000997;30687 +2426;31000415;30677 +2427;31000415;30673 +2428;31001837;30693 +2429;31001837;30690 +2430;31002390;30709 +2431;31001628;30691 +2432;31001628;30692 +2433;31002378;30712 +2434;31001868;30691 +2435;31001868;30692 +2436;31002001;30702 +2437;31001626;30691 +2438;31001626;30692 +2439;31001202;30687 +2440;31000656;30678 +2441;31000656;30672 +2442;31001452;30693 +2443;31001452;30694 +2444;31000049;30667 +2445;31001302;30688 +2446;31000509;30677 +2447;31000509;30674 +2448;31000344;30666 +2449;31000832;30677 +2450;31000832;30671 +2451;31002038;30702 +2452;31001811;30693 +2453;31001811;30692 +2454;31001622;30690 +2455;31001622;30692 +2456;31000349;30666 +2457;31000536;30677 +2458;31000536;30674 +2459;31000170;30666 +2460;31000867;30677 +2461;31000867;30671 +2462;31001282;30688 +2463;31001948;30702 +2464;31000738;30679 +2465;31000738;30675 +2466;31000271;30666 +2467;31001904;30699 +2468;31001827;30691 +2469;31001827;30693 +2470;31000702;30679 +2471;31000702;30675 +2472;31000238;30666 +2473;31001548;30693 +2474;31001548;30692 +2475;31001529;30691 +2476;31001529;30692 +2477;31001799;30691 +2478;31001799;30693 +2479;31000552;30677 +2480;31000552;30674 +2481;31001002;30687 +2482;31001156;30695 +2483;31000180;30666 +2484;31000818;30677 +2485;31000818;30671 +2486;31001704;30691 +2487;31001704;30693 +2488;31000561;30678 +2489;31000561;30672 +2490;31000188;30666 +2491;31000182;30666 +2492;31000620;30678 +2493;31000620;30672 +2494;31000575;30678 +2495;31000575;30672 +2496;31000520;30677 +2497;31000520;30674 +2498;31000626;30678 +2499;31000626;30672 +2500;31000453;30677 +2501;31000453;30673 +2502;31001745;30691 +2503;31001745;30693 +2504;31001617;30690 +2505;31001617;30692 +2506;31001699;30691 +2507;31001699;30690 +2508;31001925;30699 +2509;31001238;30688 +2510;31000396;30677 +2511;31000396;30673 +2512;31001908;30699 +2513;31000835;30677 +2514;31000835;30671 +2515;31000084;30667 +2516;31000710;30679 +2517;31000710;30675 +2518;31000243;30666 +2519;31002219;30702 +2520;31002151;30700 +2521;31000409;30677 +2522;31000409;30673 +2523;31001276;30688 +2524;31000809;30677 +2525;31000809;30671 +2526;31000461;30677 +2527;31000461;30674 +2528;31002192;30700 +2529;31001651;30691 +2530;31001651;30692 +2531;31000843;30677 +2532;31000843;30671 +2533;31002042;30702 +2534;31002372;30708 +2535;31000337;30666 +2536;31000948;30687 +2537;31000760;30677 +2538;31000760;30671 +2539;31001515;30689 +2540;31001515;30692 +2541;31001616;30690 +2542;31001616;30692 +2543;31001099;30695 +2544;31001793;30693 +2545;31001793;30690 +2546;31000974;30687 +2547;31000857;30677 +2548;31000857;30671 +2549;31001213;30687 +2550;31000670;30678 +2551;31000670;30672 +2552;31001298;30688 +2553;31000254;30666 +2554;31001482;30689 +2555;31001482;30692 +2556;31002217;30702 +2557;31000981;30687 +2558;31000385;30677 +2559;31000385;30673 +2560;31000227;30666 +2561;31001836;30693 +2562;31001836;30690 +2563;31000521;30677 +2564;31000521;30674 +2565;31001400;30690 +2566;31001400;30689 +2567;31001303;30688 +2568;31001945;30702 +2569;31001514;30689 +2570;31001514;30692 +2571;31001200;30687 +2572;31001747;30691 +2573;31001747;30693 +2574;31001233;30688 +2575;31001927;30699 +2576;31002025;30702 +2577;31002458;30711 +2578;31002478;30711 +2579;31000911;30687 +2580;31000681;30678 +2581;31000681;30672 +2582;31002271;30702 +2583;31002397;30707 +2584;31001559;30693 +2585;31001559;30692 +2586;31000176;30666 +2587;31000210;30666 +2588;31000882;30687 +2589;31002148;30700 +2590;31001157;30695 +2591;31002365;30701 +2592;31002115;30703 +2593;31002464;30711 +2594;31000987;30687 +2595;31001050;30687 +2596;31000908;30687 +2597;31001425;30693 +2598;31001425;30690 +2599;31000335;30666 +2600;31000595;30678 +2601;31000595;30672 +2602;31002083;30703 +2603;31001236;30688 +2604;31000305;30666 +2605;31001872;30691 +2606;31001872;30692 +2607;31001619;30690 +2608;31001619;30692 +2609;31000421;30677 +2610;31000421;30673 +2611;31001478;30693 +2612;31001478;30689 +2613;31001856;30691 +2614;31001856;30692 +2615;31000111;30667 +2616;31001230;30688 +2617;31002385;30711 +2618;31000781;30677 +2619;31000781;30671 +2620;31001583;30691 +2621;31001583;30692 +2622;31001644;30691 +2623;31001644;30692 +2624;31001043;30687 +2625;31001557;30693 +2626;31001557;30692 +2627;31001981;30702 +2628;31001830;30691 +2629;31001830;30693 +2630;31001653;30691 +2631;31001653;30692 +2632;31002427;30710 +2633;31000568;30678 +2634;31000568;30672 +2635;31001921;30699 +2636;31001191;30687 +2637;31001251;30688 +2638;31000424;30677 +2639;31000424;30673 +2640;31001880;30702 +2641;31000587;30678 +2642;31000587;30672 +2643;31002279;30702 +2644;31002070;30703 +2645;31000195;30666 +2646;31001132;30695 +2647;31000648;30678 +2648;31000648;30672 +2649;31000628;30678 +2650;31000628;30672 +2651;31000031;30668 +2652;31000778;30677 +2653;31000778;30671 +2654;31000793;30677 +2655;31000793;30671 +2656;31001319;30687 +2657;31001107;30695 +2658;31001984;30702 +2659;31000903;30687 +2660;31002324;30701 +2661;31001832;30691 +2662;31001832;30693 +2663;31001542;30694 +2664;31001542;30692 +2665;31001917;30702 +2666;31001406;30693 +2667;31001406;30690 +2668;31001464;30691 +2669;31001464;30689 +2670;31002474;30711 +2671;31002263;30702 +2672;31000559;30677 +2673;31000559;30674 +2674;31000608;30678 +2675;31000608;30672 +2676;31000284;30666 +2677;31001946;30702 +2678;31001421;30691 +2679;31001421;30690 +2680;31002124;30703 +2681;31002093;30703 +2682;31000913;30687 +2683;31000171;30666 +2684;31000381;30677 +2685;31000381;30673 +2686;31000169;30666 +2687;31001246;30688 +2688;31000404;30677 +2689;31000404;30673 +2690;31001862;30691 +2691;31001862;30690 +2692;31002027;30702 +2693;31002307;30701 +2694;31001283;30688 +2695;31000939;30687 +2696;31002133;30700 +2697;31002423;30709 +2698;31000075;30667 +2699;31001649;30691 +2700;31001649;30692 +2701;31001541;30694 +2702;31001541;30692 +2703;31001085;30695 +2704;31002155;30700 +2705;31000398;30677 +2706;31000398;30673 +2707;31000060;30667 +2708;31001819;30693 +2709;31001819;30690 +2710;31000806;30677 +2711;31000806;30671 +2712;31001768;30691 +2713;31001768;30693 +2714;31000300;30666 +2715;31001084;30695 +2716;31001853;30691 +2717;31001853;30692 +2718;31001274;30688 +2719;31002493;30703 +2720;31000512;30677 +2721;31000512;30674 +2722;31001042;30687 +2723;31000019;30668 +2724;31000534;30677 +2725;31000534;30674 +2726;31001550;30693 +2727;31001550;30692 +2728;31002216;30702 +2729;31000528;30677 +2730;31000528;30674 +2731;31001809;30693 +2732;31001809;30692 +2733;31001451;30693 +2734;31001451;30694 +2735;31001137;30695 +2736;31000966;30687 +2737;31001566;30694 +2738;31001566;30692 +2739;31001590;30693 +2740;31001590;30692 +2741;31002014;30702 +2742;31001916;30702 +2743;31000612;30678 +2744;31000612;30672 +2745;31000770;30677 +2746;31000770;30671 +2747;31001846;30693 +2748;31001846;30694 +2749;31002413;30712 +2750;31001614;30690 +2751;31001614;30692 +2752;31002063;30703 +2753;31001797;30691 +2754;31001797;30693 +2755;31000517;30677 +2756;31000517;30674 +2757;31001062;30687 +2758;31002072;30703 +2759;31001145;30695 +2760;31000751;30679 +2761;31000751;30676 +2762;31001818;30693 +2763;31001818;30690 +2764;31001160;30695 +2765;31000116;30667 +2766;31000029;30668 +2767;31000206;30666 +2768;31000667;30678 +2769;31000667;30672 +2770;31002143;30700 +2771;31001034;30687 +2772;31001087;30695 +2773;31001627;30691 +2774;31001627;30692 +2775;31000314;30666 +2776;31002402;30707 +2777;31001680;30691 +2778;31001680;30690 +2779;31001715;30691 +2780;31001715;30689 +2781;31002350;30701 +2782;31000441;30677 +2783;31000441;30673 +2784;31002399;30707 +2785;31001605;30690 +2786;31001605;30692 +2787;31001475;30693 +2788;31001475;30689 +2789;31001442;30691 +2790;31001442;30694 +2791;31001291;30688 +2792;31001503;30691 +2793;31001503;30689 +2794;31001353;30687 +2795;31000024;30668 +2796;31000062;30667 +2797;31000276;30666 +2798;31001821;30693 +2799;31001821;30692 +2800;31001488;30690 +2801;31001488;30689 +2802;31001060;30687 +2803;31001285;30688 +2804;31000651;30678 +2805;31000651;30672 +2806;31001951;30702 +2807;31000456;30677 +2808;31000456;30673 +2809;31000379;30677 +2810;31000379;30673 +2811;31000826;30677 +2812;31000826;30671 +2813;31002426;30710 +2814;31001509;30690 +2815;31001509;30692 +2816;31001902;30699 +2817;31001301;30688 +2818;31000371;30677 +2819;31000371;30673 +2820;31002048;30702 +2821;31001754;30691 +2822;31001754;30694 +2823;31000336;30666 +2824;31002368;30708 +2825;31001980;30702 +2826;31001072;30687 +2827;31001997;30702 +2828;31002328;30701 +2829;31000191;30666 +2830;31001776;30693 +2831;31001776;30692 +2832;31000477;30677 +2833;31000477;30674 +2834;31001738;30691 +2835;31001738;30693 +2836;31001732;30691 +2837;31001732;30692 +2838;31000077;30667 +2839;31001688;30691 +2840;31001688;30692 +2841;31001248;30688 +2842;31000769;30677 +2843;31000769;30671 +2844;31001744;30691 +2845;31001744;30693 +2846;31001375;30690 +2847;31001375;30692 +2848;31000669;30678 +2849;31000669;30672 +2850;31000257;30666 +2851;31000531;30677 +2852;31000531;30674 +2853;31001401;30690 +2854;31001401;30689 +2855;31000309;30666 +2856;31000177;30666 +2857;31000386;30677 +2858;31000386;30673 +2859;31001395;30690 +2860;31001395;30692 +2861;31001543;30694 +2862;31001543;30692 +2863;31001545;30694 +2864;31001545;30692 +2865;31000037;30667 +2866;31002073;30703 +2867;31002288;30701 +2868;31001372;30687 +2869;31000880;30687 +2870;31000145;30666 +2871;31000414;30677 +2872;31000414;30673 +2873;31002410;30710 +2874;31001093;30695 +2875;31000408;30677 +2876;31000408;30673 +2877;31001474;30693 +2878;31001474;30689 +2879;31001239;30688 +2880;31000146;30666 +2881;31000010;30668 +2882;31000954;30687 +2883;31002398;30707 +2884;31000619;30678 +2885;31000619;30672 +2886;31001831;30691 +2887;31001831;30693 +2888;31000376;30677 +2889;31000376;30673 +2890;31001851;30693 +2891;31001851;30694 +2892;31001915;30702 +2893;31001571;30694 +2894;31001571;30692 +2895;31001187;30687 +2896;31000308;30666 +2897;31000149;30666 +2898;31001448;30693 +2899;31001448;30694 +2900;31000744;30679 +2901;31000744;30676 +2902;31001324;30687 +2903;31002469;30711 +2904;31000564;30678 +2905;31000564;30672 +2906;31000078;30667 +2907;31002179;30700 +2908;31001364;30687 +2909;31002408;30710 +2910;31000895;30687 +2911;31000329;30666 +2912;31001938;30698 +2913;31001772;30693 +2914;31001772;30692 +2915;31000724;30679 +2916;31000724;30675 +2917;31001286;30688 +2918;31001056;30687 +2919;31000237;30666 +2920;31000218;30666 +2921;31002200;30702 +2922;31000837;30677 +2923;31000837;30671 +2924;31002119;30703 +2925;31000334;30666 +2926;31000922;30687 +2927;31002157;30700 +2928;31002040;30702 +2929;31002472;30702 +2930;31001396;30690 +2931;31001396;30689 +2932;31000142;30666 +2933;31001535;30691 +2934;31001535;30692 +2935;31002254;30702 +2936;31000317;30666 +2937;31001243;30688 +2938;31002240;30702 +2939;31002345;30701 +2940;31002109;30703 +2941;31001329;30687 +2942;31000871;30677 +2943;31000871;30671 +2944;31002153;30700 +2945;31000224;30666 +2946;31002282;30702 +2947;31000798;30677 +2948;31000798;30671 +2949;31001773;30693 +2950;31001773;30692 +2951;31001460;30691 +2952;31001460;30689 +2953;31001650;30691 +2954;31001650;30692 +2955;31001553;30693 +2956;31001553;30692 +2957;31001875;30691 +2958;31001875;30692 +2959;31001597;30693 +2960;31001597;30692 +2961;31000873;30677 +2962;31000873;30671 +2963;31002210;30702 +2964;31000499;30677 +2965;31000499;30674 +2966;31001128;30695 +2967;31001064;30687 +2968;31000652;30678 +2969;31000652;30672 +2970;31000829;30677 +2971;31000829;30671 +2972;31000026;30668 +2973;31002340;30701 +2974;31002393;30709 +2975;31001604;30690 +2976;31001604;30692 +2977;31001897;30698 +2978;31000110;30667 +2979;31001678;30691 +2980;31001678;30690 +2981;31000458;30677 +2982;31000458;30673 +2983;31000433;30677 +2984;31000433;30673 +2985;31002126;30700 +2986;31001689;30691 +2987;31001689;30692 +2988;31001537;30691 +2989;31001537;30692 +2990;31001866;30691 +2991;31001866;30692 +2992;31001674;30691 +2993;31001674;30690 +2994;31002351;30701 +2995;31000219;30666 +2996;31000541;30677 +2997;31000541;30674 +2998;31000570;30678 +2999;31000570;30672 +3000;31000272;30666 +3001;31000046;30667 +3002;31001718;30691 +3003;31001718;30689 +3004;31002439;30711 +3005;31000783;30677 +3006;31000783;30671 +3007;31002121;30703 +3008;31001574;30691 +3009;31001574;30692 +3010;31001665;30691 +3011;31001665;30693 +3012;31000637;30678 +3013;31000637;30672 +3014;31000761;30677 +3015;31000761;30671 +3016;31000967;30687 +3017;31001131;30695 +3018;31000253;30666 +3019;31001493;30689 +3020;31001493;30692 +3021;31002012;30702 +3022;31000236;30666 +3023;31001117;30695 +3024;31001822;30693 +3025;31001822;30692 +3026;31000139;30667 +3027;31000600;30678 +3028;31000600;30672 +3029;31001216;30687 +3030;31000714;30679 +3031;31000714;30675 +3032;31002100;30703 +3033;31001717;30691 +3034;31001717;30689 +3035;31001714;30691 +3036;31001714;30689 +3037;31000926;30687 +3038;31001159;30695 +3039;31000978;30687 +3040;31000854;30677 +3041;31000854;30671 +3042;31000505;30677 +3043;31000505;30674 +3044;31001891;30698 +3045;31000018;30668 +3046;31001273;30688 +3047;31000435;30677 +3048;31000435;30673 +3049;31000956;30687 +3050;31002019;30702 +3051;31001178;30687 +3052;31000980;30687 +3053;31000355;30677 +3054;31000355;30673 +3055;31001967;30702 +3056;31000992;30687 +3057;31001181;30687 +3058;31000548;30677 +3059;31000548;30674 +3060;31002021;30702 +3061;31001804;30691 +3062;31001804;30693 +3063;31000123;30667 +3064;31002363;30701 +3065;31002102;30703 +3066;31000683;30678 +3067;31000683;30672 +3068;31002094;30703 +3069;31001756;30691 +3070;31001756;30694 +3071;31002465;30711 +3072;31000003;30672 +3073;31001007;30687 +3074;31001252;30688 +3075;31002236;30702 +3076;31002369;30708 +3077;31001269;30688 +3078;31000285;30666 +3079;31000322;30666 +3080;31000027;30668 +3081;31000614;30678 +3082;31000614;30672 +3083;31001633;30690 +3084;31001633;30692 +3085;31001937;30698 +3086;31002061;30703 +3087;31000841;30677 +3088;31000841;30671 +3089;31000098;30667 +3090;31000553;30677 +3091;31000553;30674 +3092;31000445;30677 +3093;31000445;30673 +3094;31001063;30687 +3095;31000928;30687 +3096;31000845;30677 +3097;31000845;30671 +3098;31000865;30677 +3099;31000865;30671 +3100;31001461;30691 +3101;31001461;30689 +3102;31000181;30666 +3103;31001781;30691 +3104;31001781;30693 +3105;31001787;30693 +3106;31001787;30692 +3107;31001035;30687 +3108;31001123;30695 +3109;31000720;30679 +3110;31000720;30675 +3111;31002305;30701 +3112;31000286;30666 +3113;31002208;30702 +3114;31001327;30687 +3115;31000936;30687 +3116;31001402;30690 +3117;31001402;30689 +3118;31002293;30701 +3119;31001443;30691 +3120;31001443;30694 +3121;31001991;30702 +3122;31000468;30677 +3123;31000468;30674 +3124;31000834;30677 +3125;31000834;30671 +3126;31001487;30690 +3127;31001487;30689 +3128;31002227;30702 +3129;31001570;30694 +3130;31001570;30692 +3131;31001879;30691 +3132;31001879;30689 +3133;31002415;30712 +3134;31001637;30690 +3135;31001637;30692 +3136;31000789;30677 +3137;31000789;30671 +3138;31001565;30693 +3139;31001565;30692 +3140;31000297;30666 +3141;31001995;30702 +3142;31000917;30687 +3143;31000983;30687 +3144;31001021;30687 +3145;31001391;30690 +3146;31001391;30692 +3147;31001385;30690 +3148;31001385;30692 +3149;31001223;30688 +3150;31000175;30666 +3151;31001483;30689 +3152;31001483;30692 +3153;31001172;30695 +3154;31002226;30702 +3155;31001795;30691 +3156;31001795;30693 +3157;31002007;30702 +3158;31002054;30702 +3159;31000896;30687 +3160;31000492;30677 +3161;31000492;30674 +3162;31000319;30666 +3163;31002409;30710 +3164;31001947;30702 +3165;31001839;30693 +3166;31001839;30690 +3167;31001146;30695 +3168;31001328;30687 +3169;31001780;30691 +3170;31001780;30693 +3171;31002483;30699 +3172;31001390;30690 +3173;31001390;30692 +3174;31001966;30702 +3175;31001318;30687 +3176;31002334;30701 +3177;31001690;30691 +3178;31001690;30692 +3179;31002091;30703 +3180;31001333;30687 +3181;31001091;30695 +3182;31002310;30701 +3183;31001673;30691 +3184;31001673;30690 +3185;31001071;30687 +3186;31001888;30702 +3187;31000269;30666 +3188;31001315;30688 +3189;31000543;30677 +3190;31000543;30674 +3191;31000576;30678 +3192;31000576;30672 +3193;31001749;30691 +3194;31001749;30693 +3195;31000092;30667 +3196;31001300;30688 +3197;31000988;30687 +3198;31002333;30701 +3199;31001028;30687 +3200;31002256;30702 +3201;31002035;30702 +3202;31001030;30687 +3203;31002468;30711 +3204;31002259;30702 +3205;31000260;30666 +3206;31001996;30702 +3207;31000213;30666 +3208;31002114;30703 +3209;31000855;30677 +3210;31000855;30671 +3211;31002099;30703 +3212;31001600;30690 +3213;31001600;30692 +3214;31002482;30702 +3215;31000754;30677 +3216;31000754;30671 +3217;31000339;30666 +3218;31001480;30689 +3219;31001480;30692 +3220;31001447;30691 +3221;31001447;30694 +3222;31001403;30693 +3223;31001403;30690 +3224;31002420;30709 +3225;31001253;30688 +3226;31000452;30677 +3227;31000452;30673 +3228;31001767;30691 +3229;31001767;30693 +3230;31000454;30677 +3231;31000454;30673 +3232;31000804;30677 +3233;31000804;30671 +3234;31001193;30687 +3235;31001039;30687 +3236;31001410;30693 +3237;31001410;30690 +3238;31001941;30698 +3239;31000129;30667 +3240;31000162;30666 +3241;31002283;30701 +3242;31000311;30666 +3243;31001081;30695 +3244;31000047;30667 +3245;31001321;30687 +3246;31001477;30693 +3247;31001477;30689 +3248;31000264;30666 +3249;31001310;30688 +3250;31001138;30695 +3251;31002181;30700 +3252;31002266;30702 +3253;31000321;30666 +3254;31000155;30666 +3255;31000635;30678 +3256;31000635;30672 +3257;31000449;30677 +3258;31000449;30673 +3259;31001284;30688 +3260;31002002;30702 +3261;31001779;30691 +3262;31001779;30693 +3263;31001520;30691 +3264;31001520;30692 +3265;31000484;30677 +3266;31000484;30674 +3267;31000487;30677 +3268;31000487;30674 +3269;31001263;30688 +3270;31001048;30687 +3271;31001016;30687 +3272;31001275;30688 +3273;31002274;30702 +3274;31002452;30711 +3275;31001368;30687 +3276;31000122;30667 +3277;31001502;30691 +3278;31001502;30689 +3279;31001808;30693 +3280;31001808;30692 +3281;31002144;30700 +3282;31001883;30702 +3283;31002255;30702 +3284;31000768;30677 +3285;31000768;30671 +3286;31001115;30695 +3287;31000991;30687 +3288;31000786;30677 +3289;31000786;30671 +3290;31000602;30678 +3291;31000602;30672 +3292;31001241;30688 +3293;31002222;30702 +3294;31002326;30701 +3295;31001782;30691 +3296;31001782;30693 +3297;31001994;30702 +3298;31000313;30666 +3299;31001037;30687 +3300;31000132;30667 +3301;31000556;30677 +3302;31000556;30674 +3303;31000006;30672 +3304;31000004;30672 +3305;31001077;30695 +3306;31000842;30677 +3307;31000842;30671 +3308;31001494;30689 +3309;31001494;30692 +3310;31001124;30695 +3311;31001208;30687 +3312;31000644;30678 +3313;31000644;30672 +3314;31001685;30691 +3315;31001685;30692 +3316;31001344;30687 +3317;31002191;30700 +3318;31000166;30666 +3319;31001607;30693 +3320;31001607;30692 +3321;31000609;30678 +3322;31000609;30672 +3323;31001470;30690 +3324;31001470;30689 +3325;31000025;30668 +3326;31001378;30690 +3327;31001378;30692 +3328;31001217;30687 +3329;31001736;30691 +3330;31001736;30693 +3331;31000251;30666 +3332;31000892;30687 +3333;31001194;30687 +3334;31002446;30711 +3335;31001923;30699 +3336;31001095;30695 +3337;31002139;30700 +3338;31000423;30677 +3339;31000423;30673 +3340;31001906;30699 +3341;31001884;30702 +3342;31000015;30668 +3343;31001360;30687 +3344;31001708;30691 +3345;31001708;30693 +3346;31002332;30701 +3347;31001730;30691 +3348;31001730;30692 +3349;31001522;30691 +3350;31001522;30692 +3351;31000862;30677 +3352;31000862;30671 +3353;31000412;30677 +3354;31000412;30673 +3355;31000090;30667 +3356;31001288;30688 +3357;31001376;30690 +3358;31001376;30692 +3359;31002338;30701 +3360;31000808;30677 +3361;31000808;30671 +3362;31000392;30677 +3363;31000392;30673 +3364;31002211;30702 +3365;31000941;30687 +3366;31000708;30679 +3367;31000708;30675 +3368;31001433;30691 +3369;31001433;30690 +3370;31000383;30677 +3371;31000383;30673 +3372;31001304;30688 +3373;31001504;30691 +3374;31001504;30689 +3375;31001606;30693 +3376;31001606;30692 +3377;31002450;30711 +3378;31001812;30693 +3379;31001812;30692 +3380;31002079;30703 +3381;31001309;30688 +3382;31001647;30691 +3383;31001647;30692 +3384;31000551;30677 +3385;31000551;30674 +3386;31000463;30677 +3387;31000463;30674 +3388;31000382;30677 +3389;31000382;30673 +3390;31001225;30688 +3391;31001632;30690 +3392;31001632;30692 +3393;31001560;30693 +3394;31001560;30692 +3395;31001199;30687 +3396;31002173;30700 +3397;31001260;30688 +3398;31001999;30702 +3399;31000641;30678 +3400;31000641;30672 +3401;31002342;30701 +3402;31001136;30695 +3403;31000490;30677 +3404;31000490;30674 +3405;31000957;30687 +3406;31001935;30698 +3407;31000324;30666 +3408;31001499;30689 +3409;31001499;30692 +3410;31000288;30666 +3411;31000719;30679 +3412;31000719;30675 +3413;31002412;30712 +3414;31001798;30691 +3415;31001798;30693 +3416;31000968;30687 +3417;31001180;30687 +3418;31001670;30691 +3419;31001670;30690 +3420;31001563;30693 +3421;31001563;30692 +3422;31000341;30666 +3423;31000775;30677 +3424;31000775;30671 +3425;31001471;30690 +3426;31001471;30689 +3427;31000323;30666 +3428;31001998;30702 +3429;31000498;30677 +3430;31000498;30674 +3431;31001305;30688 +3432;31000791;30677 +3433;31000791;30671 +3434;31002082;30703 +3435;31001591;30693 +3436;31001591;30692 +3437;31002163;30700 +3438;31001909;30699 +3439;31000179;30666 +3440;31002480;30701 +3441;31001173;30695 +3442;31001277;30688 +3443;31001711;30691 +3444;31001711;30693 +3445;31002360;30701 +3446;31001326;30687 +3447;31000158;30666 +3448;31001588;30693 +3449;31001588;30692 +3450;31002031;30702 +3451;31000245;30666 +3452;31001933;30699 +3453;31000443;30677 +3454;31000443;30673 +3455;31002425;30709 +3456;31001465;30691 +3457;31001465;30689 +3458;31001829;30691 +3459;31001829;30693 +3460;31000258;30666 +3461;31002344;30701 +3462;31002185;30700 +3463;31001250;30688 +3464;31002285;30701 +3465;31000979;30687 +3466;31000577;30678 +3467;31000577;30672 +3468;31001510;30690 +3469;31001510;30692 +3470;31001974;30702 +3471;31001697;30691 +3472;31001697;30690 +3473;31000100;30667 +3474;31001235;30688 +3475;31000973;30687 +3476;31002034;30702 +3477;31000662;30678 +3478;31000662;30672 +3479;31000748;30679 +3480;31000748;30676 +3481;31001183;30687 +3482;31001394;30690 +3483;31001394;30692 +3484;31000601;30678 +3485;31000601;30672 +3486;31000655;30678 +3487;31000655;30672 +3488;31000673;30678 +3489;31000673;30672 +3490;31000705;30679 +3491;31000705;30675 +3492;31002239;30702 +3493;31001709;30691 +3494;31001709;30693 +3495;31000105;30667 +3496;31002110;30703 +3497;31000771;30677 +3498;31000771;30671 +3499;31002085;30703 +3500;31002355;30701 +3501;31001348;30687 +3502;31001972;30702 +3503;31000167;30666 +3504;31000233;30666 +3505;31001801;30691 +3506;31001801;30693 +3507;31000483;30677 +3508;31000483;30674 +3509;31002251;30702 +3510;31001871;30691 +3511;31001871;30692 +3512;31000540;30677 +3513;31000540;30674 +3514;31001661;30691 +3515;31001661;30689 +3516;31000266;30666 +3517;31001668;30691 +3518;31001668;30693 +3519;31000136;30667 +3520;31002388;30711 +3521;31001596;30693 +3522;31001596;30692 +3523;31000698;30678 +3524;31000698;30672 +3525;31001555;30693 +3526;31001555;30692 +3527;31001857;30691 +3528;31001857;30692 +3529;31000884;30687 +3530;31001320;30687 +3531;31001153;30695 +3532;31002140;30700 +3533;31000223;30666 +3534;31000953;30687 +3535;31001009;30687 +3536;31002036;30702 +3537;31001020;30687 +3538;31001014;30687 +3539;31000034;30668 +3540;31002056;30702 +3541;31000717;30679 +3542;31000717;30675 +3543;31002490;30700 +3544;31000938;30687 +3545;31001105;30695 +3546;31001954;30702 +3547;31001101;30695 +3548;31001575;30691 +3549;31001575;30692 +3550;31001389;30690 +3551;31001389;30692 +3552;31002381;30711 +3553;31000919;30687 +3554;31001176;30695 +3555;31001855;30691 +3556;31001855;30692 +3557;31000094;30667 +3558;31001068;30687 +3559;31002471;30701 +3560;31002495;30711 +3561;31001192;30687 +3562;31000361;30677 +3563;31000361;30673 +3564;31000692;30678 +3565;31000692;30672 +3566;31001913;30699 +3567;31000023;30668 +3568;31001899;30699 +3569;31000291;30666 +3570;31000876;30677 +3571;31000876;30671 +3572;31001112;30695 +3573;31000998;30687 +3574;31001631;30690 +3575;31001631;30692 +3576;31000607;30678 +3577;31000607;30672 +3578;31000610;30678 +3579;31000610;30672 +3580;31002431;30710 +3581;31000746;30679 +3582;31000746;30676 +3583;31000696;30678 +3584;31000696;30672 +3585;31000802;30677 +3586;31000802;30671 +3587;31001185;30687 +3588;31000278;30666 +3589;31001151;30695 +3590;31000207;30666 +3591;31000086;30667 +3592;31001860;30691 +3593;31001860;30690 +3594;31001656;30691 +3595;31001656;30692 +3596;31000583;30678 +3597;31000583;30672 +3598;31000247;30666 +3599;31001800;30691 +3600;31001800;30693 +3601;31000782;30677 +3602;31000782;30671 +3603;31001435;30693 +3604;31001435;30694 +3605;31000687;30678 +3606;31000687;30672 +3607;31000045;30667 +3608;31001188;30687 +3609;31001229;30688 +3610;31000112;30667 +3611;31001850;30693 +3612;31001850;30694 +3613;31002486;30698 +3614;31001695;30691 +3615;31001695;30690 +3616;31001119;30695 +3617;31000971;30687 +3618;31000009;30668 +3619;31001658;30691 +3620;31001658;30689 +3621;31000740;30679 +3622;31000740;30675 +3623;31001752;30691 +3624;31001752;30693 +3625;31001636;30690 +3626;31001636;30692 +3627;31000569;30678 +3628;31000569;30672 +3629;31001220;30687 +3630;31001726;30691 +3631;31001726;30692 +3632;31001003;30687 +3633;31001852;30691 +3634;31001852;30692 +3635;31000063;30667 +3636;31000684;30678 +3637;31000684;30672 +3638;31001010;30687 +3639;31002162;30700 +3640;31002395;30709 +3641;31000524;30677 +3642;31000524;30674 +3643;31002357;30701 +3644;31001001;30687 +3645;31002273;30702 +3646;31001898;30699 +3647;31001384;30690 +3648;31001384;30692 +3649;31002484;30711 +3650;31001257;30688 +3651;31001271;30688 +3652;31002314;30701 +3653;31002265;30702 +3654;31001120;30695 +3655;31001970;30702 +3656;31000431;30677 +3657;31000431;30673 +3658;31000225;30666 +3659;31002317;30701 +3660;31000582;30678 +3661;31000582;30672 +3662;31001177;30695 +3663;31001232;30688 +3664;31000338;30666 +3665;31001473;30693 +3666;31001473;30689 +3667;31000495;30677 +3668;31000495;30674 +3669;31000135;30667 +3670;31001174;30695 +3671;31001438;30693 +3672;31001438;30694 +3673;31000632;30678 +3674;31000632;30672 +3675;31001663;30691 +3676;31001663;30693 +3677;31000428;30677 +3678;31000428;30673 +3679;31000580;30678 +3680;31000580;30672 +3681;31000633;30678 +3682;31000633;30672 +3683;31000849;30677 +3684;31000849;30671 +3685;31002455;30711 +3686;31000604;30678 +3687;31000604;30672 +3688;31000840;30677 +3689;31000840;30671 +3690;31001914;30702 +3691;31000803;30677 +3692;31000803;30671 +3693;31001254;30688 +3694;31000055;30667 +3695;31000193;30666 +3696;31002267;30702 +3697;31000403;30677 +3698;31000403;30673 +3699;31001144;30695 +3700;31002030;30702 +3701;31002364;30701 +3702;31000592;30678 +3703;31000592;30672 +3704;31001986;30702 +3705;31000028;30668 +3706;31000208;30666 +3707;31001867;30691 +3708;31001867;30692 +3709;31002339;30701 +3710;31000402;30677 +3711;31000402;30673 +3712;31000071;30667 +3713;31000858;30677 +3714;31000858;30671 +3715;31001306;30688 +3716;31002108;30703 +3717;31002005;30702 +3718;31001104;30695 +3719;31000057;30667 +3720;31000764;30677 +3721;31000764;30671 +3722;31001686;30691 +3723;31001686;30692 +3724;31002300;30701 +3725;31002235;30702 +3726;31002329;30701 +3727;31002000;30702 +3728;31001454;30694 +3729;31001454;30692 +3730;31000584;30678 +3731;31000584;30672 +3732;31000457;30677 +3733;31000457;30673 +3734;31002289;30701 +3735;31000877;30677 +3736;31000877;30671 +3737;31000870;30677 +3738;31000870;30671 +3739;31002494;30702 +3740;31000515;30677 +3741;31000515;30674 +3742;31000261;30666 +3743;31002186;30700 +3744;31000444;30677 +3745;31000444;30673 +3746;31001519;30689 +3747;31001519;30692 +3748;31002212;30702 +3749;31000389;30677 +3750;31000389;30673 +3751;31001294;30688 +3752;31000393;30677 +3753;31000393;30673 +3754;31000165;30666 +3755;31000450;30677 +3756;31000450;30673 +3757;31002224;30702 +3758;31000827;30677 +3759;31000827;30671 +3760;31001129;30695 +3761;31001296;30688 +3762;31000682;30678 +3763;31000682;30672 +3764;31000642;30678 +3765;31000642;30672 +3766;31000660;30678 +3767;31000660;30672 +3768;31001386;30690 +3769;31001386;30692 +3770;31000005;34370 +3771;31000005;34368 +3772;31000005;34369 \ No newline at end of file diff --git a/export/csv/wormhole.csv b/export/csv/wormhole.csv index b2274e5a..262c1886 100644 --- a/export/csv/wormhole.csv +++ b/export/csv/wormhole.csv @@ -1,90 +1,90 @@ -"Id";"Name";"Security";"MassTotal";"MassIndividual";"MassRegeneration";"MaxStableTime";"SignatureStrength"; -"1";"A009";"C13";"500000000";"5000000";"3000000000";"16";; -"2";"A239";"L";"2000000000";"300000000";;"24";"5"; -"3";"A641";"H";"2000000000";"1000000000";;"16";"10"; -"4";"A982";"C6";"3000000000";"300000000";;"24";"2.22"; -"5";"B041";"C6";"3000000000";"300000000";"500000000";"48";; -"6";"B274";"H";"2000000000";"300000000";;"24";"10"; -"7";"B449";"H";"2000000000";"1000000000";;"16";"2.5"; -"8";"B520";"H";"3000000000";"300000000";"500000000";"24";; -"9";"C008";"C5";"1000000000";"5000000";"3000000000";"16";; -"10";"C125";"C2";"1000000000";"20000000";;"16";"6.67"; -"11";"C140";"L";"3000000000";"1350000000";;"24";"5"; -"12";"C247";"C3";"2000000000";"300000000";;"16";"10"; -"13";"C248";"0.0";"3000000000";"1350000000";"500000000";"24";; -"14";"C391";"L";"3000000000";"1000000000";"500000000";"24";; -"15";"D364";"C2";"1000000000";"300000000";;"16";"1.25"; -"16";"D382";"C2";"2000000000";"300000000";;"16";"6.67"; -"17";"D792";"H";"3000000000";"1000000000";;"24";"2.5"; -"18";"D845";"H";"5000000000";"300000000";"500000000";"24";"5"; -"19";"E004";"C1";"1000000000";"5000000";"3000000000";"16";; -"20";"E175";"C4";"2000000000";"300000000";;"16";"5"; -"21";"E545";"0.0";"2000000000";"300000000";;"24";"2.5"; -"23";"G024";"C2";"2000000000";"300000000";;"16";"1.25"; -"24";"H121";"C1";"500000000";"20000000";;"16";"10"; -"25";"H296";"C5";"3000000000";"1350000000";;"24";"10"; -"26";"H900";"C5";"3000000000";"300000000";;"24";"2.5"; -"27";"I182";"C2";"2000000000";"300000000";;"16";"4"; -"28";"J244";"L";"1000000000";"20000000";;"24";"5"; -"29";"K329";"0.0";"5000000000";"1800000000";"500000000";"24";; -"30";"K346";"0.0";"3000000000";"300000000";;"24";"2.5"; -"31";"L005";"C2";"1000000000";"5000000";"3000000000";"16";; -"32";"L477";"C3";"2000000000";"300000000";;"16";"5"; -"33";"L614";"C5";"1000000000";"20000000";;"24";"2.5"; -"35";"M267";"C3";"1000000000";"300000000";;"16";"1.25"; -"36";"M555";"C5";"3000000000";"1000000000";;"24";"2.5"; -"37";"M609";"C4";"1000000000";"20000000";;"16";"4"; -"38";"N062";"C5";"3000000000";"300000000";;"24";"2.5"; -"39";"N110";"H";"1000000000";"20000000";;"24";"10"; -"40";"N290";"L";"3000000000";"1350000000";"500000000";"24";; -"41";"N432";"C5";"3000000000";"1350000000";;"24";"10"; -"42";"N766";"C2";"2000000000";"300000000";;"16";"4"; -"43";"N770";"C5";"3000000000";"300000000";;"24";"2.5"; -"44";"N944";"L";"3000000000";"1350000000";;"24";"10"; -"45";"N968";"C3";"2000000000";"300000000";;"16";"10"; -"46";"O128";"C4";"1000000000";"300000000";"100000000";"24";; -"47";"O477";"C3";"2000000000";"300000000";;"16";"5"; -"48";"O883";"C3";"1000000000";"20000000";;"16";"5"; -"49";"P060";"C1";"500000000";"20000000";;"16";"5"; -"50";"Q003";"0.0";"1000000000";"5000000";"3000000000";"16";; -"51";"Q317";"C1";"500000000";"20000000";;"16";"2.5"; -"52";"R051";"L";"3000000000";"1000000000";;"16";"5"; -"53";"R474";"C6";"3000000000";"300000000";;"24";"2.22"; -"54";"R943";"C2";"750000000";"300000000";;"16";"6.67"; -"55";"S047";"H";"3000000000";"300000000";;"24";; -"56";"S199";"0.0";"3000000000";"1350000000";;"24";"10"; -"57";"S804";"C6";"1000000000";"20000000";;"24";"1.25"; -"58";"T405";"C4";"2000000000";"300000000";;"16";"6.67"; -"59";"U210";"L";"3000000000";"300000000";;"24";"10"; -"60";"U319";"C6";"3000000000";"1350000000";"500000000";"48";; -"61";"U574";"C6";"3000000000";"300000000";;"24";"1.25"; -"62";"V283";"0.0";"3000000000";"1000000000";;"24";"2.5"; -"63";"V301";"C1";"500000000";"20000000";;"16";"5"; -"64";"V753";"C6";"3000000000";"1350000000";;"24";"6.67"; -"65";"V911";"C5";"3000000000";"1350000000";;"24";"10"; -"66";"W237";"C6";"3000000000";"1350000000";;"24";"6.67"; -"67";"X702";"C3";"1000000000";"300000000";;"24";"10"; -"68";"X877";"C4";"2000000000";"300000000";;"16";"6.67"; -"69";"Y683";"C4";"2000000000";"300000000";;"16";"4"; -"70";"Y790";"C1";"500000000";"20000000";;"16";"2.5"; -"71";"Z006";"C3";"1000000000";"5000000";"3000000000";"16";; -"72";"Z060";"0.0";"1000000000";"20000000";;"24";"2.5"; -"73";"Z142";"0.0";"3000000000";"1350000000";;"24";"10"; -"74";"Z457";"C4";"2000000000";"300000000";;"16";"4"; -"75";"Z647";"C1";"500000000";"20000000";;"16";"10"; -"76";"Z971";"C1";"100000000";"20000000";;"16";"10"; -"80";"M001";"C4";"1000000000";"5000000";"3000000000";"16";; -"81";"E587";"0.0";"3000000000";"1000000000";;"16";; -"82";"V898";"L";"2000000000";"300000000";;"16";; -"83";"Q063";"H";"500000000";"20000000";;"16";; -"84";"G008";"C6";"1000000000";"5000000";"3000000000";"16";; -"85";"F353";"C12";"100000000";"20000000";;"16";; -"86";"F135";"C12";"750000000";"300000000";;"16";; -"87";"T458";"C12";"500000000";"20000000";;"16";; -"88";"M164";"C12";"2000000000";"300000000";;"16";; -"89";"L031";"C12";"3000000000";"1000000000";;"16";; -"90";"S877";"C14";"750000000";"300000000";;"16";; -"91";"B735";"C15";"750000000";"300000000";;"16";; -"92";"V928";"C16";"750000000";"300000000";;"16";; -"93";"C414";"C17";"750000000";"300000000";;"16";; -"94";"R259";"C18";"750000000";"300000000";;"16";; \ No newline at end of file +Id;Name;scanWormholeStrength +1;A009; +2;A239;5 +3;A641;10 +4;A982;2.22 +5;B041; +6;B274;10 +7;B449;2.5 +8;B520; +9;C008; +10;C125;6.67 +11;C140;5 +12;C247;10 +13;C248; +14;C391; +15;D364;1.25 +16;D382;6.67 +17;D792;2.5 +18;D845;5 +19;E004; +20;E175;5 +21;E545;2.5 +23;G024;1.25 +24;H121;10 +25;H296;10 +26;H900;2.5 +27;I182;4 +28;J244;5 +29;K329; +30;K346;2.5 +31;L005; +32;L477;5 +33;L614;2.5 +35;M267;1.25 +36;M555;2.5 +37;M609;4 +38;N062;2.5 +39;N110;10 +40;N290; +41;N432;10 +42;N766;4 +43;N770;2.5 +44;N944;10 +45;N968;10 +46;O128; +47;O477;5 +48;O883;5 +49;P060;5 +50;Q003; +51;Q317;2.5 +52;R051;5 +53;R474;2.22 +54;R943;6.67 +55;S047; +56;S199;10 +57;S804;1.25 +58;T405;6.67 +59;U210;10 +60;U319; +61;U574;1.25 +62;V283;2.5 +63;V301;5 +64;V753;6.67 +65;V911;10 +66;W237;6.67 +67;X702;10 +68;X877;6.67 +69;Y683;4 +70;Y790;2.5 +71;Z006; +72;Z060;2.5 +73;Z142;10 +74;Z457;4 +75;Z647;10 +76;Z971;10 +80;M001; +81;E587; +82;V898; +83;Q063; +84;G008; +85;F353; +86;F135; +87;T458; +88;M164; +89;L031; +90;S877; +91;B735; +92;V928; +93;C414; +94;R259; \ No newline at end of file diff --git a/export/sql/eve_universe.sql.zip b/export/sql/eve_universe.sql.zip index f88481dc..ba4d3bb7 100644 Binary files a/export/sql/eve_universe.sql.zip and b/export/sql/eve_universe.sql.zip differ diff --git a/js/app.js b/js/app.js index ec82a8b4..12379702 100644 --- a/js/app.js +++ b/js/app.js @@ -27,7 +27,7 @@ requirejs.config({ admin: './app/admin', // initial start "admin page" view notification: './app/notification', // "notification" view - jquery: 'lib/jquery-3.3.1.min', // v3.3.1 jQuery + jquery: 'lib/jquery-3.4.1.min', // v3.4.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', // v3.0.1 Javascript template engine - http://mustache.github.io @@ -42,11 +42,11 @@ requirejs.config({ 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.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 + bootbox: 'lib/bootbox.min', // v5.2.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 + hoverIntent: 'lib/jquery.hoverIntent.min', // v1.10.0 Hover intention - http://cherne.net/brian/resources/jquery.hoverIntent.html 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 diff --git a/js/app/conf/signature_type.js b/js/app/conf/signature_type.js index 0e0727f5..108da68b 100644 --- a/js/app/conf/signature_type.js +++ b/js/app/conf/signature_type.js @@ -383,6 +383,51 @@ define(['jquery'], ($) => { 30: 'K346 - 0.0', 31: 'Z060 - 0.0' } + }, + 14: { // Drifter Sentinel WH + 1: { // Combat + 1: 'Monolith', + 2: 'Wormhole in Rock Circle', + 3: 'Opposing Spatial Rifts', + 4: 'Sleeper Enclave Debris', + 5: 'Crystal Resource' + } + }, + 15: { // Drifter Barbican WH + 1: { // Combat + 1: 'Wrecked Ships', + 2: 'Unstable Wormhole', + 3: 'Spatial Rift', + 4: 'Heavily Guarded Spatial Rift', + 5: 'Crystals' + } + }, + 16: { // Drifter Vidette WH + 1: { // Combat + 1: 'Ship Graveyard', + 2: 'Sleeper Engineering Station', + 3: 'Spatial Rift', + 4: 'Sleeper Enclave in Coral Rock', + 5: 'Crystals and Stone Circle' + } + }, + 17: { // Drifter Conflux WH + 1: { // Combat + 1: 'Monolith', + 2: 'Caged Wormhole', + 3: 'Rock Formation and Wormhole', + 4: 'Particle Acceleration Array', + 5: 'Guarded Asteroid Station' + } + }, + 18: { // Drifter Redoubt WH + 1: { // Combat + 1: 'Ship Graveyard', + 2: 'Caged Wormhole', + 3: 'Spatial Rift Generator', + 4: 'Sleeper Enclave', + 5: 'Hollow Asteroid' + } } }, // system type (k-space) 2: { diff --git a/js/app/conf/system_effect.js b/js/app/conf/system_effect.js index 44215ade..2fc3f8a1 100644 --- a/js/app/conf/system_effect.js +++ b/js/app/conf/system_effect.js @@ -4,730 +4,820 @@ */ -define([], function(){ +define([], () => { 'use strict'; - // system effects - let systemEffects = { - wh: { - magnetar: { - 1: [ - { - effect: 'Damage', - value: '+30%' - },{ - effect: 'Missile exp. radius', - value: '+15%' - },{ - effect: 'Drone tracking', - value: '-15%' - },{ - effect: 'Targeting range', - value: '-15%' - },{ - effect: 'Tracking speed', - value: '-15%' - },{ - effect: 'Target Painter strength', - value: '-15%' - } - ], - 2: [ - { - effect: 'Damage', - value: '+44%' - },{ - effect: 'Missile exp. radius', - value: '+22%' - },{ - effect: 'Drone tracking', - value: '-22%' - },{ - effect: 'Targeting range', - value: '-22%' - },{ - effect: 'Tracking speed', - value: '-22%' - },{ - effect: 'Target Painter strength', - value: '-22%' - } - ], - 3: [ - { - effect: 'Damage', - value: '+58%' - },{ - effect: 'Missile exp. radius', - value: '+29%' - },{ - effect: 'Drone tracking', - value: '-29%' - },{ - effect: 'Targeting range', - value: '-29%' - },{ - effect: 'Tracking speed', - value: '-29%' - },{ - effect: 'Target Painter strength', - value: '-29%' - } - ], - 4: [ - { - effect: 'Damage', - value: '+72%' - },{ - effect: 'Missile exp. radius', - value: '+36%' - },{ - effect: 'Drone tracking', - value: '-36%' - },{ - effect: 'Targeting range', - value: '-36%' - },{ - effect: 'Tracking speed', - value: '-36%' - },{ - effect: 'Target Painter strength', - value: '-36%' - } - ], - 5: [ - { - effect: 'Damage', - value: '+86%' - },{ - effect: 'Missile exp. radius', - value: '+43%' - },{ - effect: 'Drone tracking', - value: '-43%' - },{ - effect: 'Targeting range', - value: '-43%' - },{ - effect: 'Tracking speed', - value: '-43%' - },{ - effect: 'Target Painter strength', - value: '-43%' - } - ], - 6: [ - { - effect: 'Damage', - value: '+100%' - },{ - effect: 'Missile exp. radius', - value: '+50%' - },{ - effect: 'Drone tracking', - value: '-50%' - },{ - effect: 'Targeting range', - value: '-50%' - },{ - effect: 'Tracking speed', - value: '-50%' - },{ - effect: 'Target Painter strength', - value: '-50%' - } - ] - }, - redGiant: { - 1: [ - { - effect: 'Heat damage', - value: '+15%' - },{ - effect: 'Overload bonus', - value: '+30%' - },{ - effect: 'Smart Bomb range', - value: '+30%' - },{ - effect: 'Smart Bomb damage', - value: '+30%' - },{ - effect: 'Bomb damage', - value: '+30%' - } - ], - 2: [ - { - effect: 'Heat damage', - value: '+22%' - },{ - effect: 'Overload bonus', - value: '+44%' - },{ - effect: 'Smart Bomb range', - value: '+44%' - },{ - effect: 'Smart Bomb damage', - value: '+44%' - },{ - effect: 'Bomb damage', - value: '+44%' - } - ], - 3: [ - { - effect: 'Heat damage', - value: '+29%' - },{ - effect: 'Overload bonus', - value: '+58%' - },{ - effect: 'Smart Bomb range', - value: '+58%' - },{ - effect: 'Smart Bomb damage', - value: '+58%' - },{ - effect: 'Bomb damage', - value: '+58%' - } - ], - 4: [ - { - effect: 'Heat damage', - value: '+36%' - },{ - effect: 'Overload bonus', - value: '+72%' - },{ - effect: 'Smart Bomb range', - value: '+72%' - },{ - effect: 'Smart Bomb damage', - value: '+72%' - },{ - effect: 'Bomb damage', - value: '+72%' - } - ], - 5: [ - { - effect: 'Heat damage', - value: '+43%' - },{ - effect: 'Overload bonus', - value: '+86%' - },{ - effect: 'Smart Bomb range', - value: '+86%' - },{ - effect: 'Smart Bomb damage', - value: '+86%' - },{ - effect: 'Bomb damage', - value: '+86%' - } - ], - 6: [ - { - effect: 'Heat damage', - value: '+50%' - },{ - effect: 'Overload bonus', - value: '+100%' - },{ - effect: 'Smart Bomb range', - value: '+100%' - },{ - effect: 'Smart Bomb damage', - value: '+100%' - },{ - effect: 'Bomb damage', - value: '+100%' - } - ] - }, - pulsar: { - 1: [ - { - effect: 'Shield HP', - value: '+30%' - },{ - effect: 'Armor resist', - value: '-15%' - },{ - effect: 'Capacitor recharge', - value: '-15%' - },{ - effect: 'Signature', - value: '+30%' - },{ - effect: 'NOS/Neut drain', - value: '+30%' - } - ], - 2: [ - { - effect: 'Shield HP', - value: '+44%' - },{ - effect: 'Armor resist', - value: '-22%' - },{ - effect: 'Capacitor recharge', - value: '-22%' - },{ - effect: 'Signature', - value: '+44%' - },{ - effect: 'NOS/Neut drain', - value: '+44%' - } - ], - 3: [ - { - effect: 'Shield HP', - value: '+58%' - },{ - effect: 'Armor resist', - value: '-29%' - },{ - effect: 'Capacitor recharge', - value: '-29%' - },{ - effect: 'Signature', - value: '+58%' - },{ - effect: 'NOS/Neut drain', - value: '+58%' - } - ], - 4: [ - { - effect: 'Shield HP', - value: '+72%' - },{ - effect: 'Armor resist', - value: '-36%' - },{ - effect: 'Capacitor recharge', - value: '-36%' - },{ - effect: 'Signature', - value: '+72%' - },{ - effect: 'NOS/Neut drain', - value: '+72%' - } - ], - 5: [ - { - effect: 'Shield HP', - value: '+86%' - },{ - effect: 'Armor resist', - value: '-43%' - },{ - effect: 'Capacitor recharge', - value: '-43%' - },{ - effect: 'Signature', - value: '+86%' - },{ - effect: 'NOS/Neut drain', - value: '+86%' - } - ], - 6: [ - { - effect: 'Shield HP', - value: '+100%' - },{ - effect: 'Armor resist', - value: '-50%' - },{ - effect: 'Capacitor recharge', - value: '-50%' - },{ - effect: 'Signature', - value: '+100%' - },{ - effect: 'NOS/Neut drain', - value: '+100%' - } - ] - }, - wolfRayet: { - 1: [ - { - effect: 'Armor HP', - value: '+30%' - },{ - effect: 'Shield resist', - value: '-15%' - },{ - effect: 'Small Weapon damage', - value: '+60%' - },{ - effect: 'Signature size', - value: '-15%' - } - ], - 2: [ - { - effect: 'Armor HP', - value: '+44%' - },{ - effect: 'Shield resist', - value: '-22%' - },{ - effect: 'Small Weapon damage', - value: '+88%' - },{ - effect: 'Signature size', - value: '-22%' - } - ], - 3: [ - { - effect: 'Armor HP', - value: '+58%' - },{ - effect: 'Shield resist', - value: '-29%' - },{ - effect: 'Small Weapon damage', - value: '+116%' - },{ - effect: 'Signature size', - value: '-29%' - } - ], - 4: [ - { - effect: 'Armor HP', - value: '+72%' - },{ - effect: 'Shield resist', - value: '-36%' - },{ - effect: 'Small Weapon damage', - value: '+144%' - },{ - effect: 'Signature size', - value: '-36%' - } - ], - 5: [ - { - effect: 'Armor HP', - value: '+86%' - },{ - effect: 'Shield resist', - value: '-43%' - },{ - effect: 'Small Weapon damage', - value: '+172%' - },{ - effect: 'Signature size', - value: '-43%' - } - ], - 6: [ - { - effect: 'Armor HP', - value: '+100%' - },{ - effect: 'Shield resist', - value: '-50%' - },{ - effect: 'Small Weapon damage', - value: '+200%' - },{ - effect: 'Signature size', - value: '-50%' - } - ] - }, - cataclysmic: { - 1: [ - { - effect: 'Local armor repair amount', - value: '-15%' - },{ - effect: 'Local shield boost amount', - value: '-15%' - },{ - effect: 'Shield transfer amount', - value: '+30%' - },{ - effect: 'Remote repair amount', - value: '+30%' - },{ - effect: 'Capacitor capacity', - value: '+30%' - },{ - effect: 'Capacitor recharge time', - value: '+15%' - },{ - effect: 'Remote Capacitor Transmitter amount', - value: '-15%' - } - ], - 2: [ - { - effect: 'Local armor repair amount', - value: '-22%' - },{ - effect: 'Local shield boost amount', - value: '-22%' - },{ - effect: 'Shield transfer amount', - value: '+44%' - },{ - effect: 'Remote repair amount', - value: '+44%' - },{ - effect: 'Capacitor capacity', - value: '+44%' - },{ - effect: 'Capacitor recharge time', - value: '+22%' - },{ - effect: 'Remote Capacitor Transmitter amount', - value: '-22%' - } - ], - 3: [ - { - effect: 'Local armor repair amount', - value: '-29%' - },{ - effect: 'Local shield boost amount', - value: '-29%' - },{ - effect: 'Shield transfer amount', - value: '+58%' - },{ - effect: 'Remote repair amount', - value: '+58%' - },{ - effect: 'Capacitor capacity', - value: '+58%' - },{ - effect: 'Capacitor recharge time', - value: '+29%' - },{ - effect: 'Remote Capacitor Transmitter amount', - value: '-29%' - } - ], - 4: [ - { - effect: 'Local armor repair amount', - value: '-36%' - },{ - effect: 'Local shield boost amount', - value: '-36%' - },{ - effect: 'Shield transfer amount', - value: '+72%' - },{ - effect: 'Remote repair amount', - value: '+72%' - },{ - effect: 'Capacitor capacity', - value: '+72%' - },{ - effect: 'Capacitor recharge time', - value: '+36%' - },{ - effect: 'Remote Capacitor Transmitter amount', - value: '-36%' - } - ], - 5: [ - { - effect: 'Local armor repair amount', - value: '-43%' - },{ - effect: 'Local shield boost amount', - value: '-43%' - },{ - effect: 'Shield transfer amount', - value: '+86%' - },{ - effect: 'Remote repair amount', - value: '+86%' - },{ - effect: 'Capacitor capacity', - value: '+86%' - },{ - effect: 'Capacitor recharge time', - value: '+43%' - },{ - effect: 'Remote Capacitor Transmitter amount', - value: '-43%' - } - ], - 6: [ - { - effect: 'Local armor repair amount', - value: '-50%' - },{ - effect: 'Local shield boost amount', - value: '-50%' - },{ - effect: 'Shield transfer amount', - value: '+100%' - },{ - effect: 'Remote repair amount', - value: '+100%' - },{ - effect: 'Capacitor capacity', - value: '+100%' - },{ - effect: 'Capacitor recharge time', - value: '+50%' - },{ - effect: 'Remote Capacitor Transmitter amount', - value: '-50%' - } - ] - }, - blackHole: { - 1: [ - { - effect: 'Missile velocity', - value: '+15%' - },{ - effect: 'Missile exp. velocity', - value: '+30%' - },{ - effect: 'Ship velocity', - value: '+30%' - },{ - effect: 'Stasis Webifier strength', - value: '-15%' - },{ - effect: 'Inertia', - value: '+15%' - },{ - effect: 'Targeting range', - value: '+30%' - } - ], - 2: [ - { - effect: 'Missile velocity', - value: '+22%' - },{ - effect: 'Missile exp. velocity', - value: '+44%' - },{ - effect: 'Ship velocity', - value: '+44%' - },{ - effect: 'Stasis Webifier strength', - value: '-22%' - },{ - effect: 'Inertia', - value: '+22%' - },{ - effect: 'Targeting range', - value: '+44%' - } - ], - 3: [ - { - effect: 'Missile velocity', - value: '+29%' - },{ - effect: 'Missile exp. velocity', - value: '+58%' - },{ - effect: 'Ship velocity', - value: '+58%' - },{ - effect: 'Stasis Webifier strength', - value: '-29%' - },{ - effect: 'Inertia', - value: '+29%' - },{ - effect: 'Targeting range', - value: '+58%' - } - ], - 4: [ - { - effect: 'Missile velocity', - value: '+36%' - },{ - effect: 'Missile exp. velocity', - value: '+72%' - },{ - effect: 'Ship velocity', - value: '+72%' - },{ - effect: 'Stasis Webifier strength', - value: '-36%' - },{ - effect: 'Inertia', - value: '+36%' - },{ - effect: 'Targeting range', - value: '+72%' - } - ], - 5: [ - { - effect: 'Missile velocity', - value: '+43%' - },{ - effect: 'Missile exp. velocity', - value: '+86%' - },{ - effect: 'Ship velocity', - value: '+86%' - },{ - effect: 'Stasis Webifier strength', - value: '-43%' - },{ - effect: 'Inertia', - value: '+43%' - },{ - effect: 'Targeting range', - value: '+86%' - } - ], - 6: [ - { - effect: 'Missile velocity', - value: '+50%' - },{ - effect: 'Missile exp. velocity', - value: '+100%' - },{ - effect: 'Ship velocity', - value: '+100%' - },{ - effect: 'Stasis Webifier strength', - value: '-50%' - },{ - effect: 'Inertia', - value: '+50%' - },{ - effect: 'Targeting range', - value: '+100%' - } - ] - } + /** + * get system effect multiplier + * @param areaId + * @returns {number} + */ + let getMultiplierByAreaId = areaId => { + let multiply = 0; + switch(areaId){ + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + multiply = areaId; // C1-C6 holes + break; + case 13: + multiply = 6; // Shattered frigate holes + break; + case 14: + case 15: + case 16: + case 17: + case 18: + multiply = 2; // Drifter space + break; + } + + return multiply; + }; + + + let magnetar = { + 1: [ + { + effect: 'Damage', + value: '+30%' + }, { + effect: 'Missile exp. radius', + value: '+15%' + }, { + effect: 'Drone tracking', + value: '-15%' + }, { + effect: 'Targeting range', + value: '-15%' + }, { + effect: 'Tracking speed', + value: '-15%' + }, { + effect: 'Target Painter strength', + value: '-15%' } - }; + ], + 2: [ + { + effect: 'Damage', + value: '+44%' + }, { + effect: 'Missile exp. radius', + value: '+22%' + }, { + effect: 'Drone tracking', + value: '-22%' + }, { + effect: 'Targeting range', + value: '-22%' + }, { + effect: 'Tracking speed', + value: '-22%' + }, { + effect: 'Target Painter strength', + value: '-22%' + } + ], + 3: [ + { + effect: 'Damage', + value: '+58%' + }, { + effect: 'Missile exp. radius', + value: '+29%' + }, { + effect: 'Drone tracking', + value: '-29%' + }, { + effect: 'Targeting range', + value: '-29%' + }, { + effect: 'Tracking speed', + value: '-29%' + }, { + effect: 'Target Painter strength', + value: '-29%' + } + ], + 4: [ + { + effect: 'Damage', + value: '+72%' + }, { + effect: 'Missile exp. radius', + value: '+36%' + }, { + effect: 'Drone tracking', + value: '-36%' + }, { + effect: 'Targeting range', + value: '-36%' + }, { + effect: 'Tracking speed', + value: '-36%' + }, { + effect: 'Target Painter strength', + value: '-36%' + } + ], + 5: [ + { + effect: 'Damage', + value: '+86%' + }, { + effect: 'Missile exp. radius', + value: '+43%' + }, { + effect: 'Drone tracking', + value: '-43%' + }, { + effect: 'Targeting range', + value: '-43%' + }, { + effect: 'Tracking speed', + value: '-43%' + }, { + effect: 'Target Painter strength', + value: '-43%' + } + ], + 6: [ + { + effect: 'Damage', + value: '+100%' + }, { + effect: 'Missile exp. radius', + value: '+50%' + }, { + effect: 'Drone tracking', + value: '-50%' + }, { + effect: 'Targeting range', + value: '-50%' + }, { + effect: 'Tracking speed', + value: '-50%' + }, { + effect: 'Target Painter strength', + value: '-50%' + } + ] + }; + let redGiant = { + 1: [ + { + effect: 'Heat damage', + value: '+15%' + }, { + effect: 'Overload bonus', + value: '+30%' + }, { + effect: 'Smart Bomb range', + value: '+30%' + }, { + effect: 'Smart Bomb damage', + value: '+30%' + }, { + effect: 'Bomb damage', + value: '+30%' + } + ], + 2: [ + { + effect: 'Heat damage', + value: '+22%' + }, { + effect: 'Overload bonus', + value: '+44%' + }, { + effect: 'Smart Bomb range', + value: '+44%' + }, { + effect: 'Smart Bomb damage', + value: '+44%' + }, { + effect: 'Bomb damage', + value: '+44%' + } + ], + 3: [ + { + effect: 'Heat damage', + value: '+29%' + }, { + effect: 'Overload bonus', + value: '+58%' + }, { + effect: 'Smart Bomb range', + value: '+58%' + }, { + effect: 'Smart Bomb damage', + value: '+58%' + }, { + effect: 'Bomb damage', + value: '+58%' + } + ], + 4: [ + { + effect: 'Heat damage', + value: '+36%' + }, { + effect: 'Overload bonus', + value: '+72%' + }, { + effect: 'Smart Bomb range', + value: '+72%' + }, { + effect: 'Smart Bomb damage', + value: '+72%' + }, { + effect: 'Bomb damage', + value: '+72%' + } + ], + 5: [ + { + effect: 'Heat damage', + value: '+43%' + }, { + effect: 'Overload bonus', + value: '+86%' + }, { + effect: 'Smart Bomb range', + value: '+86%' + }, { + effect: 'Smart Bomb damage', + value: '+86%' + }, { + effect: 'Bomb damage', + value: '+86%' + } + ], + 6: [ + { + effect: 'Heat damage', + value: '+50%' + }, { + effect: 'Overload bonus', + value: '+100%' + }, { + effect: 'Smart Bomb range', + value: '+100%' + }, { + effect: 'Smart Bomb damage', + value: '+100%' + }, { + effect: 'Bomb damage', + value: '+100%' + } + ] + }; - return systemEffects; + let pulsar = { + 1: [ + { + effect: 'Shield HP', + value: '+30%' + }, { + effect: 'Armor resist', + value: '-15%' + }, { + effect: 'Capacitor recharge', + value: '-15%' + }, { + effect: 'Signature', + value: '+30%' + }, { + effect: 'NOS/Neut drain', + value: '+30%' + } + ], + 2: [ + { + effect: 'Shield HP', + value: '+44%' + }, { + effect: 'Armor resist', + value: '-22%' + }, { + effect: 'Capacitor recharge', + value: '-22%' + }, { + effect: 'Signature', + value: '+44%' + }, { + effect: 'NOS/Neut drain', + value: '+44%' + } + ], + 3: [ + { + effect: 'Shield HP', + value: '+58%' + }, { + effect: 'Armor resist', + value: '-29%' + }, { + effect: 'Capacitor recharge', + value: '-29%' + }, { + effect: 'Signature', + value: '+58%' + }, { + effect: 'NOS/Neut drain', + value: '+58%' + } + ], + 4: [ + { + effect: 'Shield HP', + value: '+72%' + }, { + effect: 'Armor resist', + value: '-36%' + }, { + effect: 'Capacitor recharge', + value: '-36%' + }, { + effect: 'Signature', + value: '+72%' + }, { + effect: 'NOS/Neut drain', + value: '+72%' + } + ], + 5: [ + { + effect: 'Shield HP', + value: '+86%' + }, { + effect: 'Armor resist', + value: '-43%' + }, { + effect: 'Capacitor recharge', + value: '-43%' + }, { + effect: 'Signature', + value: '+86%' + }, { + effect: 'NOS/Neut drain', + value: '+86%' + } + ], + 6: [ + { + effect: 'Shield HP', + value: '+100%' + }, { + effect: 'Armor resist', + value: '-50%' + }, { + effect: 'Capacitor recharge', + value: '-50%' + }, { + effect: 'Signature', + value: '+100%' + }, { + effect: 'NOS/Neut drain', + value: '+100%' + } + ] + }; + + let wolfRayet = { + 1: [ + { + effect: 'Armor HP', + value: '+30%' + }, { + effect: 'Shield resist', + value: '-15%' + }, { + effect: 'Small Weapon damage', + value: '+60%' + }, { + effect: 'Signature size', + value: '-15%' + } + ], + 2: [ + { + effect: 'Armor HP', + value: '+44%' + }, { + effect: 'Shield resist', + value: '-22%' + }, { + effect: 'Small Weapon damage', + value: '+88%' + }, { + effect: 'Signature size', + value: '-22%' + } + ], + 3: [ + { + effect: 'Armor HP', + value: '+58%' + }, { + effect: 'Shield resist', + value: '-29%' + }, { + effect: 'Small Weapon damage', + value: '+116%' + }, { + effect: 'Signature size', + value: '-29%' + } + ], + 4: [ + { + effect: 'Armor HP', + value: '+72%' + }, { + effect: 'Shield resist', + value: '-36%' + }, { + effect: 'Small Weapon damage', + value: '+144%' + }, { + effect: 'Signature size', + value: '-36%' + } + ], + 5: [ + { + effect: 'Armor HP', + value: '+86%' + }, { + effect: 'Shield resist', + value: '-43%' + }, { + effect: 'Small Weapon damage', + value: '+172%' + }, { + effect: 'Signature size', + value: '-43%' + } + ], + 6: [ + { + effect: 'Armor HP', + value: '+100%' + }, { + effect: 'Shield resist', + value: '-50%' + }, { + effect: 'Small Weapon damage', + value: '+200%' + }, { + effect: 'Signature size', + value: '-50%' + } + ] + }; + + let cataclysmic = { + 1: [ + { + effect: 'Local armor repair amount', + value: '-15%' + }, { + effect: 'Local shield boost amount', + value: '-15%' + }, { + effect: 'Shield transfer amount', + value: '+30%' + }, { + effect: 'Remote repair amount', + value: '+30%' + }, { + effect: 'Capacitor capacity', + value: '+30%' + }, { + effect: 'Capacitor recharge time', + value: '+15%' + }, { + effect: 'Remote Capacitor Transmitter amount', + value: '-15%' + } + ], + 2: [ + { + effect: 'Local armor repair amount', + value: '-22%' + }, { + effect: 'Local shield boost amount', + value: '-22%' + }, { + effect: 'Shield transfer amount', + value: '+44%' + }, { + effect: 'Remote repair amount', + value: '+44%' + }, { + effect: 'Capacitor capacity', + value: '+44%' + }, { + effect: 'Capacitor recharge time', + value: '+22%' + }, { + effect: 'Remote Capacitor Transmitter amount', + value: '-22%' + } + ], + 3: [ + { + effect: 'Local armor repair amount', + value: '-29%' + }, { + effect: 'Local shield boost amount', + value: '-29%' + }, { + effect: 'Shield transfer amount', + value: '+58%' + }, { + effect: 'Remote repair amount', + value: '+58%' + }, { + effect: 'Capacitor capacity', + value: '+58%' + }, { + effect: 'Capacitor recharge time', + value: '+29%' + }, { + effect: 'Remote Capacitor Transmitter amount', + value: '-29%' + } + ], + 4: [ + { + effect: 'Local armor repair amount', + value: '-36%' + }, { + effect: 'Local shield boost amount', + value: '-36%' + }, { + effect: 'Shield transfer amount', + value: '+72%' + }, { + effect: 'Remote repair amount', + value: '+72%' + }, { + effect: 'Capacitor capacity', + value: '+72%' + }, { + effect: 'Capacitor recharge time', + value: '+36%' + }, { + effect: 'Remote Capacitor Transmitter amount', + value: '-36%' + } + ], + 5: [ + { + effect: 'Local armor repair amount', + value: '-43%' + }, { + effect: 'Local shield boost amount', + value: '-43%' + }, { + effect: 'Shield transfer amount', + value: '+86%' + }, { + effect: 'Remote repair amount', + value: '+86%' + }, { + effect: 'Capacitor capacity', + value: '+86%' + }, { + effect: 'Capacitor recharge time', + value: '+43%' + }, { + effect: 'Remote Capacitor Transmitter amount', + value: '-43%' + } + ], + 6: [ + { + effect: 'Local armor repair amount', + value: '-50%' + }, { + effect: 'Local shield boost amount', + value: '-50%' + }, { + effect: 'Shield transfer amount', + value: '+100%' + }, { + effect: 'Remote repair amount', + value: '+100%' + }, { + effect: 'Capacitor capacity', + value: '+100%' + }, { + effect: 'Capacitor recharge time', + value: '+50%' + }, { + effect: 'Remote Capacitor Transmitter amount', + value: '-50%' + } + ] + }; + + let blackHole = { + 1: [ + { + effect: 'Missile velocity', + value: '+15%' + }, { + effect: 'Missile exp. velocity', + value: '+30%' + }, { + effect: 'Ship velocity', + value: '+30%' + }, { + effect: 'Stasis Webifier strength', + value: '-15%' + }, { + effect: 'Inertia', + value: '+15%' + }, { + effect: 'Targeting range', + value: '+30%' + } + ], + 2: [ + { + effect: 'Missile velocity', + value: '+22%' + }, { + effect: 'Missile exp. velocity', + value: '+44%' + }, { + effect: 'Ship velocity', + value: '+44%' + }, { + effect: 'Stasis Webifier strength', + value: '-22%' + }, { + effect: 'Inertia', + value: '+22%' + }, { + effect: 'Targeting range', + value: '+44%' + } + ], + 3: [ + { + effect: 'Missile velocity', + value: '+29%' + }, { + effect: 'Missile exp. velocity', + value: '+58%' + }, { + effect: 'Ship velocity', + value: '+58%' + }, { + effect: 'Stasis Webifier strength', + value: '-29%' + }, { + effect: 'Inertia', + value: '+29%' + }, { + effect: 'Targeting range', + value: '+58%' + } + ], + 4: [ + { + effect: 'Missile velocity', + value: '+36%' + }, { + effect: 'Missile exp. velocity', + value: '+72%' + }, { + effect: 'Ship velocity', + value: '+72%' + }, { + effect: 'Stasis Webifier strength', + value: '-36%' + }, { + effect: 'Inertia', + value: '+36%' + }, { + effect: 'Targeting range', + value: '+72%' + } + ], + 5: [ + { + effect: 'Missile velocity', + value: '+43%' + }, { + effect: 'Missile exp. velocity', + value: '+86%' + }, { + effect: 'Ship velocity', + value: '+86%' + }, { + effect: 'Stasis Webifier strength', + value: '-43%' + }, { + effect: 'Inertia', + value: '+43%' + }, { + effect: 'Targeting range', + value: '+86%' + } + ], + 6: [ + { + effect: 'Missile velocity', + value: '+50%' + }, { + effect: 'Missile exp. velocity', + value: '+100%' + }, { + effect: 'Ship velocity', + value: '+100%' + }, { + effect: 'Stasis Webifier strength', + value: '-50%' + }, { + effect: 'Inertia', + value: '+50%' + }, { + effect: 'Targeting range', + value: '+100%' + } + ] + }; + + // system effects + return { + getMultiplierByAreaId: getMultiplierByAreaId, + wh: { + magnetar: { + 1: magnetar[getMultiplierByAreaId(1)], + 2: magnetar[getMultiplierByAreaId(2)], + 3: magnetar[getMultiplierByAreaId(3)], + 4: magnetar[getMultiplierByAreaId(4)], + 5: magnetar[getMultiplierByAreaId(5)], + 6: magnetar[getMultiplierByAreaId(6)], + 16: magnetar[getMultiplierByAreaId(16)] + }, + redGiant: { + 1: redGiant[getMultiplierByAreaId(1)], + 2: redGiant[getMultiplierByAreaId(2)], + 3: redGiant[getMultiplierByAreaId(3)], + 4: redGiant[getMultiplierByAreaId(4)], + 5: redGiant[getMultiplierByAreaId(5)], + 6: redGiant[getMultiplierByAreaId(6)], + 14: redGiant[getMultiplierByAreaId(14)] + }, + pulsar: { + 1: pulsar[getMultiplierByAreaId(1)], + 2: pulsar[getMultiplierByAreaId(2)], + 3: pulsar[getMultiplierByAreaId(3)], + 4: pulsar[getMultiplierByAreaId(4)], + 5: pulsar[getMultiplierByAreaId(5)], + 6: pulsar[getMultiplierByAreaId(6)], + 17: pulsar[getMultiplierByAreaId(17)] + }, + wolfRayet: { + 1: wolfRayet[getMultiplierByAreaId(1)], + 2: wolfRayet[getMultiplierByAreaId(2)], + 3: wolfRayet[getMultiplierByAreaId(3)], + 4: wolfRayet[getMultiplierByAreaId(4)], + 5: wolfRayet[getMultiplierByAreaId(5)], + 6: wolfRayet[getMultiplierByAreaId(6)], + 13: wolfRayet[getMultiplierByAreaId(13)], + 18: wolfRayet[getMultiplierByAreaId(18)] + }, + cataclysmic: { + 1: cataclysmic[getMultiplierByAreaId(1)], + 2: cataclysmic[getMultiplierByAreaId(2)], + 3: cataclysmic[getMultiplierByAreaId(3)], + 4: cataclysmic[getMultiplierByAreaId(4)], + 5: cataclysmic[getMultiplierByAreaId(5)], + 6: cataclysmic[getMultiplierByAreaId(6)], + 15: cataclysmic[getMultiplierByAreaId(15)] + }, + blackHole: { + 1: blackHole[getMultiplierByAreaId(1)], + 2: blackHole[getMultiplierByAreaId(2)], + 3: blackHole[getMultiplierByAreaId(3)], + 4: blackHole[getMultiplierByAreaId(4)], + 5: blackHole[getMultiplierByAreaId(5)], + 6: blackHole[getMultiplierByAreaId(6)] + } + } + }; }); \ No newline at end of file diff --git a/js/app/counter.js b/js/app/counter.js index 134d6abc..672893ae 100644 --- a/js/app/counter.js +++ b/js/app/counter.js @@ -137,7 +137,8 @@ define([ // mark as init tableElement.attr('data-counter', 'init'); - let refreshIntervalId = window.setInterval(() => { + + let updateTableCount = () => { tableApi.cells(null, columnSelector).every(function(rowIndex, colIndex, tableLoopCount, cellLoopCount){ let cell = this; let node = cell.node(); @@ -148,7 +149,9 @@ define([ updateDateDiff( cell.nodes().to$(), date, round); } }); - }, 500); + }; + + let refreshIntervalId = window.setInterval(updateTableCount, 500); tableElement.data('interval', refreshIntervalId); }; diff --git a/js/app/datatables.loader.js b/js/app/datatables.loader.js index 38751434..b702148f 100644 --- a/js/app/datatables.loader.js +++ b/js/app/datatables.loader.js @@ -45,6 +45,7 @@ define([ table.destroyTimestampCounter(true); }); + // Status Plugin ============================================================================================== let StatusTable = function(settings){ let me = this; me.statusContainer = $('
', { diff --git a/js/app/init.js b/js/app/init.js index 19fc24a9..110d6111 100644 --- a/js/app/init.js +++ b/js/app/init.js @@ -244,6 +244,21 @@ define([], () => { }, 'C12': { class: 'pf-system-sec-special' + }, + 'C14': { + class: 'pf-system-sec-drifter' + }, + 'C15': { + class: 'pf-system-sec-drifter' + }, + 'C16': { + class: 'pf-system-sec-drifter' + }, + 'C17': { + class: 'pf-system-sec-drifter' + }, + 'C18': { + class: 'pf-system-sec-drifter' } }, // true sec @@ -627,6 +642,14 @@ define([], () => { 8: 'A009 - C13' } }, + // Drifter wormholes (can only appear in k-space) + drifterWormholes: { + 1: 'S877 - C14 Sentinel', + 2: 'B735 - C15 Barbican', + 3: 'V928 - C16 Vidette', + 4: 'C414 - C17 Conflux', + 5: 'R259 - C18 Redoubt' + }, // incoming wormholes incomingWormholes: { 1: 'K162 - C1/2/3 (unknown)', diff --git a/js/app/login.js b/js/app/login.js index 14525389..73681b6e 100644 --- a/js/app/login.js +++ b/js/app/login.js @@ -141,15 +141,14 @@ define([ // show Cookie accept hint on SSO login button let confirmationSettings = { - container: 'body', placement: 'bottom', - btnOkClass: 'btn btn-sm btn-default', - btnOkLabel: 'dismiss', - btnOkIcon: 'fas fa-fw fa-sign-in-alt', title: 'Accept cookies', btnCancelClass: 'btn btn-sm btn-success', btnCancelLabel: 'accept', btnCancelIcon: 'fas fa-fw fa-check', + btnOkClass: 'btn btn-sm btn-default', + btnOkLabel: 'dismiss', + btnOkIcon: 'fas fa-fw fa-sign-in-alt', onCancel: function(e, target){ // "Accept cookies" setAcceptCookie(); @@ -314,7 +313,6 @@ define([ } }); }); - } }); }; @@ -406,13 +404,13 @@ define([ * init scrollSpy for navigation bar */ let initScrollSpy = () => { - // init scrollspy + let scrollElement = window; + let timeout; // show elements that are currently in the viewport let showVisibleElements = () => { // find all elements that should be animated - let visibleElements = $('.' + config.animateElementClass).isInViewport(); - + let visibleElements = Util.findInViewport($('.' + config.animateElementClass)); $(visibleElements).removeClass( config.animateElementClass ); $(visibleElements).velocity('transition.flipXIn', { @@ -431,16 +429,23 @@ define([ }); }; - $( window ).scroll(() => { - // check for new visible elements - showVisibleElements(); - }); + let scrollHandler = () => { + // If there's a timer, cancel it + if(timeout){ + window.cancelAnimationFrame(timeout); + } + timeout = window.requestAnimationFrame(showVisibleElements); + }; + + scrollElement.addEventListener('scroll', scrollHandler, false); // initial check for visible elements showVisibleElements(); - // event listener for navigation links - Util.initPageScroll('#' + config.navigationElementId); + + Util.initScrollSpy(document.getElementById(config.navigationElementId), scrollElement, { + offset: 150 + }); }; /** @@ -449,11 +454,18 @@ define([ */ let initServerStatus = () => { $.ajax({ - type: 'POST', + type: 'GET', url: Init.path.getServerStatus, dataType: 'json' }).done(function(responseData, textStatus, request){ + let dateLastModified = new Date(request.getResponseHeader('Last-Modified') || Date.now()); + let dateExpires = new Date(request.getResponseHeader('Expires') || Date.now()); + + var options = { hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'UTC', timeZoneName: 'short' }; + responseData.api.cache = dateLastModified.toLocaleTimeString('en-US', options); + responseData.api.cacheExpire = 'TTL ' + (dateExpires - dateLastModified) / 1000 + 's'; + let data = { stickyPanelServerId: config.stickyPanelServerId, stickyPanelClass: config.stickyPanelClass, @@ -466,7 +478,8 @@ define([ case 'online': case 'green': return 'txt-color-green'; case 'vip': - case 'yellow': return 'txt-color-orange'; + case 'yellow': return 'txt-color-yellow'; + case 'orange': return 'txt-color-orange'; case 'offline': case 'red': return 'txt-color-red'; default: return ''; diff --git a/js/app/map/contextmenu.js b/js/app/map/contextmenu.js index 4fd522ee..b05899d5 100644 --- a/js/app/map/contextmenu.js +++ b/js/app/map/contextmenu.js @@ -163,7 +163,7 @@ define([ {icon: 'fa-route', action: 'find_route', text: 'find route'}, {icon: 'fa-object-group', action: 'select_connections', text: 'select connections'}, {icon: 'fa-reply fa-rotate-180', text: 'waypoints', subitems: [ - {subIcon: 'fa-flag-checkered', subAction: 'set_destination', subText: 'set destination'}, + {subIcon: 'fa-flag', subAction: 'set_destination', subText: 'set destination'}, {subDivider: true, action: ''}, {subIcon: 'fa-step-backward', subAction: 'add_first_waypoint', subText: 'add new [start]'}, {subIcon: 'fa-step-forward', subAction: 'add_last_waypoint', subText: 'add new [end]'} diff --git a/js/app/map/layout.js b/js/app/map/layout.js index fcf1f051..2dac71af 100644 --- a/js/app/map/layout.js +++ b/js/app/map/layout.js @@ -6,41 +6,50 @@ define(() => { constructor(config){ this._defaultConfig = { container: null, // parent DOM container element - center: null, // DOM elements that works as center + center: null, // DOM element OR [x,y] coordinates that works as center elementClass: 'pf-system', // class for all elements + defaultSteps: 8, // how many potential dimensions are checked on en ellipsis around the center defaultGapX: 50, defaultGapY: 50, gapX: 50, // leave gap between elements (x-axis) gapY: 50, // leave gap between elements (y-axis) + minX: 0, // min x for valid elements + minY: 0, // min y for valid elements + spacingX: 20, // spacing x between elements + spacingY: 10, // spacing y between elements loops: 2, // max loops around "center" for search grid: false, // set to [20, 20] to force grid snapping newElementWidth: 100, // width for new element newElementHeight: 22, // height for new element + mirrorSearch: false, // if true coordinates are "mirrored" for an "alternating" search debug: false, // render debug elements + debugOk: false, // if true, only not overlapped dimensions are rendered for debug debugElementClass: 'pf-system-debug' // class for debug elements }; this._config = Object.assign({}, this._defaultConfig, config); + this._config.dimensionCache = {}; + + this._cacheKey = (dim, depth) => ['dim', dim.left, dim.top, dim.width, dim.height, depth].join('_'); + /** * convert degree into radial unit * @param deg * @returns {number} * @private */ - this._degToRad = (deg) => { - return deg * Math.PI / 180; - }; + this._degToRad = deg => deg * Math.PI / 180; /** * get element dimension/position of a DOM element * @param element - * @returns {*} + * @param spacingX + * @param spacingY + * @returns {{a: *, b: *, top: *, left: *, width: *, height: *}} * @private */ - this._getElementDimension = element => { - let dim = null; - + this._getElementDimension = (element, spacingX = 0, spacingY = 0) => { let left = 0; let top = 0; let a = 0; @@ -49,7 +58,7 @@ define(() => { let height = this._config.newElementHeight; if(Array.isArray(element)){ - // xy coordinates + // x/y coordinates let point = [ element[0] ? parseInt(element[0], 10) : 0, element[1] ? parseInt(element[1], 10) : 0 @@ -63,14 +72,21 @@ define(() => { top = point[1]; a = this._config.gapX; b = this._config.gapY; - }else if(element){ + }else if(element instanceof Element){ // DOM element - left = element.style.left ? parseInt(element.style.left, 10) : 0; - top = element.style.top ? parseInt(element.style.top, 10) : 0; - a = parseInt((element.offsetWidth / 2).toString(), 10) + this._config.gapX; - b = parseInt((element.offsetHeight / 2).toString(), 10) + this._config.gapY; - width = element.offsetWidth; - height = element.offsetHeight; + left = (element.style.left ? parseInt(element.style.left, 10) : 0) - spacingX; + top = (element.style.top ? parseInt(element.style.top, 10) : 0) - spacingY; + a = parseInt((element.offsetWidth / 2).toString(), 10) + spacingX + this._config.gapX; + b = parseInt((element.offsetHeight / 2).toString(), 10) + spacingY + this._config.gapY; + width = element.offsetWidth + 2 * spacingX; + height = element.offsetHeight + 2 * spacingY; + }else if(element instanceof Object){ + left = element.left - spacingX; + top = element.top - spacingY; + a = parseInt((element.width / 2).toString(), 10) + spacingX + this._config.gapX; + b = parseInt((element.height / 2).toString(), 10) + spacingY + this._config.gapY; + width = element.width + 2 * spacingX; + height = element.height + 2 * spacingY; } // add "gap" to a and b in order to have some space around elements @@ -121,7 +137,7 @@ define(() => { let dimensions = []; let surroundingElements = this._getContainer().getElementsByClassName(this._config.elementClass); for(let element of surroundingElements){ - dimensions.push(this._getElementDimension(element)); + dimensions.push(this._getElementDimension(element, this._config.spacingX, this._config.spacingY)); } return dimensions; }; @@ -132,7 +148,7 @@ define(() => { * @returns {*} * @private */ - this._transformPointToGrid = (point) => { + this._transformPointToGrid = point => { point[0] = Math.floor(point[0] / this._config.grid[0]) * this._config.grid[0]; point[1] = Math.floor(point[1] / this._config.grid[1]) * this._config.grid[1]; return point; @@ -231,31 +247,44 @@ define(() => { return percent; }; + /** + * checks whether dim11 has valid x/y coordinate + * -> coordinates are >= "minX/Y" limit + * @param dim1 + * @returns {*|boolean} + * @private + */ + this._valid = dim1 => dim1 && dim1.left >= this._config.minX && dim1.top >= this._config.minY; + /** * checks whether dim1 is partially overlapped by any other element * @param dim1 * @param dimensionContainer * @param allDimensions + * @param depth * @returns {boolean} * @private */ - this._isOverlapping = (dim1, dimensionContainer, allDimensions) => { + this._isOverlapping = (dim1, dimensionContainer, allDimensions, depth) => { let isOverlapping = false; if(dim1){ - if(this._percentCovered(dimensionContainer, dim1 ) === 100){ + let cacheKey = this._cacheKey(dim1, depth); + // check cache first (e.g. if grid is active some dimensions would be checked multiple times) + if(this._config.dimensionCache[cacheKey]){ + return true; + }else if(this._percentCovered(dimensionContainer, dim1) === 100){ // element is within parent container for(let dim2 of allDimensions){ let percentCovered = this._percentCovered(dim1, dim2); if(percentCovered){ isOverlapping = true; - // render debug element - this._showDebugElement(dim1, percentCovered); + this._config.dimensionCache[cacheKey] = percentCovered; break; } } }else{ isOverlapping = true; - this._showDebugElement(dim1, 100); + this._config.dimensionCache[cacheKey] = 100; } }else{ isOverlapping = true; @@ -264,36 +293,63 @@ define(() => { return isOverlapping; }; + /** + * + * @param dim1 + * @returns {boolean} + * @private + */ + this._existDimension = function(dim1){ + return ( + dim1.left === this.left && + dim1.top === this.top && + dim1.width === this.width && + dim1.height === this.height + ); + }; + /** * find all dimensions around a centerDimension that are not overlapped by other elements * @param maxResults * @param steps * @param allDimensions + * @param depth * @param loops * @returns {Array} * @private */ - this._findDimensions = (maxResults, steps, allDimensions, loops) => { + this._findDimensions = (maxResults, steps, allDimensions, depth, loops) => { + steps = steps || 1; + loops = loops || 1; + let dimensions = []; let start = 0; let end = 360; let angle = end / steps; + + // as default coordinates get checked clockwise Q4 -> Q3 -> Q2 -> Q1 + // we could also check "mirrored" coordinates Q4+Q1 -> Q3+Q2 + if(this._config.mirrorSearch){ + end /= end; + } + let dimensionContainer = this._getElementDimension(this._getContainer()); - steps = steps || 1; - loops = loops || 1; if(loops === 1){ // check center element let centerDimension = this._getElementDimension(this._config.center); - if(!this._isOverlapping(centerDimension, dimensionContainer, allDimensions)){ + if( + this._valid(centerDimension) && + !dimensions.some(this._existDimension, centerDimension) && + !this._isOverlapping(centerDimension, dimensionContainer, allDimensions, depth) + ){ dimensions.push({ left: centerDimension.left, top: centerDimension.top, width: centerDimension.width, height: centerDimension.height }); - // render debug element - this._showDebugElement(centerDimension, 0); + this._config.dimensionCache[this._cacheKey(centerDimension, depth)] = 0; maxResults--; } @@ -308,27 +364,38 @@ define(() => { while(maxResults > 0 && start < end){ // get all potential coordinates on an eclipse around a given "centerElementDimension" let coordinate = this._getEllipseCoordinates(centerDimension, end); - // transform relative x/y coordinate into a absolute 2D area - let checkDimension = this._transformCoordinate(centerDimension, coordinate); - if(!this._isOverlapping(checkDimension, dimensionContainer, allDimensions)){ - dimensions.push({ - left: checkDimension.left, - top: checkDimension.top, - width: checkDimension.width, - height: checkDimension.height - }); - // render debug element - this._showDebugElement(checkDimension, 0); - - maxResults--; + let coordinates = [coordinate]; + if(this._config.mirrorSearch && coordinate){ + coordinates.push({x: coordinate.x, y: coordinate.y * -1 }); } + + for(let coordinateTemp of coordinates){ + // transform relative x/y coordinate into a absolute 2D area + let checkDimension = this._transformCoordinate(centerDimension, coordinateTemp); + if( + this._valid(checkDimension) && + !dimensions.some(this._existDimension, checkDimension) && + !this._isOverlapping(checkDimension, dimensionContainer, allDimensions, depth) + ){ + dimensions.push({ + left: checkDimension.left, + top: checkDimension.top, + width: checkDimension.width, + height: checkDimension.height + }); + this._config.dimensionCache[this._cacheKey(checkDimension, depth)] = 0; + + maxResults--; + } + } + end -= angle; } if(maxResults > 0 && loops < this._config.loops){ loops++; steps *= 2; - dimensions = dimensions.concat(this._findDimensions(maxResults, steps, allDimensions, loops)); + dimensions = dimensions.concat(this._findDimensions(maxResults, steps, allDimensions, depth, loops)); } return dimensions; @@ -346,21 +413,33 @@ define(() => { /** * render debug element into parent container * -> checks overlapping dimension with other elements - * @param dimension - * @param percentCovered * @private */ - this._showDebugElement = (dimension, percentCovered) => { + this._showDebugElements = () => { if(this._config.debug){ - let element = document.createElement('div'); - element.style.left = dimension.left + 'px'; - element.style.top = dimension.top + 'px'; - element.style.width = dimension.width + 'px'; - element.style.height = dimension.height + 'px'; - element.style.backgroundColor = Boolean(percentCovered) ? 'rgba(255,0,0,0.1)' : 'rgba(0,255,0,0.1)'; - element.innerHTML = Math.round(percentCovered * 100) / 100 + '%'; - element.classList.add(this._config.debugElementClass); - this._getContainer().appendChild(element); + let documentFragment = document.createDocumentFragment(); + for(let [cacheKey, percentCovered] of Object.entries(this._config.dimensionCache)){ + if(this._config.debugOk && percentCovered){ + continue; + } + + let element = document.createElement('div'); + let dimParts = cacheKey.split('_'); + element.style.left = dimParts[1] + 'px'; + element.style.top = dimParts[2] + 'px'; + element.style.width = dimParts[3] + 'px'; + element.style.height = dimParts[4] + 'px'; + element.style.backgroundColor = Boolean(percentCovered) ? 'rgba(255,0,0,0.1)' : 'rgba(0,255,0,0.4)'; + element.style.opacity = Boolean(percentCovered) ? 0.5 : 1; + element.style.zIndex = Boolean(percentCovered) ? 1000 : 2000; + element.style.border = Boolean(percentCovered) ? 'none' : '1px solid rgba(0,255,0,0.3)'; + element.innerHTML = Math.round(percentCovered * 100) / 100 + ''; + element.classList.add(this._config.debugElementClass); + element.setAttribute('data-depth', dimParts[5]); + documentFragment.appendChild(element); + } + + this._getContainer().appendChild(documentFragment); } }; @@ -381,15 +460,45 @@ define(() => { /** * search for surrounding, non overlapping dimensions * @param maxResults - * @param steps + * @param findChain * @returns {Array} */ - this.findNonOverlappingDimensions = (maxResults, steps) => { + this.findNonOverlappingDimensions = (maxResults, findChain = false) => { this._hideDebugElements(); + this._config.dimensionCache = {}; + // element dimensions that exist and should be checked for overlapping let allDimensions = this._getAllElementDimensions(); + let dimensions = []; + let depth = 1; + let maxDepth = findChain ? maxResults : 1; + maxResults = findChain ? 1 : maxResults; + while(depth <= maxDepth){ + let dimensionsTemp = this._findDimensions(maxResults, this._config.defaultSteps, allDimensions, depth); - return this._findDimensions(maxResults, steps, allDimensions); + if(dimensionsTemp.length){ + dimensions = dimensions.concat(dimensionsTemp); + + if(findChain){ + // if depth > 0 we have 2D dimension as "center" (not a x/y coordinate) + // -> increase the gap + this._config.defaultGapX = 10; + this._config.defaultGapY = 10; + this._config.gapX = 50; + this._config.gapY = 50; + this._config.center = dimensionsTemp[0]; + allDimensions = allDimensions.concat([this._getElementDimension(dimensionsTemp[0], this._config.spacingX, this._config.spacingY)]); + } + + depth++; + }else{ + break; + } + } + + this._showDebugElements(); + + return dimensions; }; } } diff --git a/js/app/map/local.js b/js/app/map/local.js index eff351ec..17da0389 100644 --- a/js/app/map/local.js +++ b/js/app/map/local.js @@ -422,7 +422,7 @@ define([ _: (data, type, row, meta) => { let value = data.typeName; if(type === 'display'){ - value = ''; + value = ''; } return value; } diff --git a/js/app/map/map.js b/js/app/map/map.js index ddb1a148..1c427a2c 100644 --- a/js/app/map/map.js +++ b/js/app/map/map.js @@ -546,13 +546,16 @@ define([ system.data('region', data.region.name); system.data('constellationId', parseInt(data.constellation.id)); system.data('constellation', data.constellation.name); - system.data('faction', data.faction); system.data('planets', data.planets); system.data('shattered', data.shattered); + system.data('drifter', data.drifter); system.data('statics', data.statics); system.data('updated', parseInt(data.updated.updated)); system.data('changed', false); system.attr('data-mapid', parseInt(mapContainer.data('id'))); + if(data.sovereignty){ + system.data('sovereignty', data.sovereignty); + } // locked system if( Boolean(system.data('locked')) !== data.locked ){ @@ -660,20 +663,10 @@ define([ case 'add_system': // add new system dialog let position = Layout.getEventCoordinates(e); - - let grid = [MapUtil.config.mapSnapToGridDimension, MapUtil.config.mapSnapToGridDimension]; - let positionFinder = new Layout.Position({ - container: mapElement[0], - center: [position.x, position.y], - loops: 5, - defaultGapX: 10, - defaultGapY: 10, - grid: mapElement.hasClass(MapUtil.config.mapGridClass) ? grid : false, - debug: false + let dimensions = MapUtil.newSystemPositionByCoordinates(mapElement, { + center: [position.x, position.y] }); - let dimensions = positionFinder.findNonOverlappingDimensions(1, 8); - if(dimensions.length){ position.x = dimensions[0].left; position.y = dimensions[0].top; @@ -1212,7 +1205,7 @@ define([ mapConfig.map.setContainer(mapContainer); // init custom scrollbars and add overlay - parentElement.initMapScrollbar(); + initMapScrollbar(mapWrapper); // set map observer setMapObserver(mapConfig.map); @@ -2708,7 +2701,7 @@ define([ if(select){ let mapWrapper = mapContainer.closest('.' + config.mapWrapperClass); - Scrollbar.scrollToSystem(mapWrapper, MapUtil.getSystemPosition(system)); + Scrollbar.scrollToCenter(mapWrapper, system); // select system MapUtil.showSystemInfo(map, system); } @@ -3046,47 +3039,59 @@ define([ */ $.fn.getSystemData = function(minimal = false){ let system = $(this); + let data = system.data(); let systemData = { - id: parseInt(system.data('id')), + id: parseInt(data.id), updated: { - updated: parseInt(system.data('updated')) + updated: parseInt(data.updated) } }; if(!minimal){ - systemData = Object.assign(systemData, { - systemId: parseInt(system.data('systemId')), - name: system.data('name'), + let systemDataComplete = { + systemId: parseInt(data.systemId), + name: data.name, alias: system.getSystemInfo(['alias']), - effect: system.data('effect'), + effect: data.effect, type: { - id: system.data('typeId') + id: data.typeId }, - security: system.data('security'), - trueSec: system.data('trueSec'), + security: data.security, + trueSec: data.trueSec, region: { - id: system.data('regionId'), - name: system.data('region') + id: data.regionId, + name: data.region }, constellation: { - id: system.data('constellationId'), - name: system.data('constellation') + id: data.constellationId, + name: data.constellation }, status: { - id: system.data('statusId') + id: data.statusId }, - locked: system.data('locked') ? 1 : 0, - rallyUpdated: system.data('rallyUpdated') || 0, - rallyPoke: system.data('rallyPoke') ? 1 : 0, - currentUser: system.data('currentUser'), // if user is currently in this system - faction: system.data('faction'), - planets: system.data('planets'), - shattered: system.data('shattered') ? 1 : 0, - statics: system.data('statics'), - userCount: (system.data('userCount') ? parseInt(system.data('userCount')) : 0), + locked: data.locked ? 1 : 0, + rallyUpdated: data.rallyUpdated || 0, + rallyPoke: data.rallyPoke ? 1 : 0, + currentUser: data.currentUser, // if user is currently in this system + planets: data.planets, + shattered: data.shattered ? 1 : 0, + drifter: data.drifter ? 1 : 0, + statics: data.statics, + userCount: parseInt(data.userCount) || 0, position: MapUtil.getSystemPosition(system) - }); + }; + + let optionalDataKeys = ['sovereignty']; + + for(let dataKey of optionalDataKeys){ + let value = system.data(dataKey); + if(value !== null && value !== undefined){ + systemDataComplete[dataKey] = value; + } + } + + systemData = Object.assign(systemData, systemDataComplete); } return systemData; @@ -3166,15 +3171,13 @@ define([ /** * init scrollbar for Map element + * @param mapWrapper */ - $.fn.initMapScrollbar = function(){ - // get Map Scrollbar - let mapTabContentElement = $(this); - let mapWrapperElement = mapTabContentElement.find('.' + config.mapWrapperClass); - let mapElement = mapTabContentElement.find('.' + config.mapClass); + let initMapScrollbar = mapWrapper => { + let mapElement = mapWrapper.find('.' + config.mapClass); let mapId = mapElement.data('id'); - mapWrapperElement.initCustomScrollbar({ + Scrollbar.initScrollbar(mapWrapper, { callbacks: { onInit: function(){ // init 'space' key + 'mouse' down for map scroll ------------------------------------------------- @@ -3291,8 +3294,8 @@ define([ // ------------------------------------------------------------------------------------------------------------ // add map overlays after scrollbar is initialized // because of its absolute position - mapWrapperElement.initMapOverlays(); - mapWrapperElement.initLocalOverlay(mapId); + mapWrapper.initMapOverlays(); + mapWrapper.initLocalOverlay(mapId); }; return { diff --git a/js/app/map/overlay/overlay.js b/js/app/map/overlay/overlay.js index 8fdb7c81..582e8e20 100644 --- a/js/app/map/overlay/overlay.js +++ b/js/app/map/overlay/overlay.js @@ -370,15 +370,16 @@ define([ // init popover if not already exists if(!systemHead.data('bs.popover')){ let system = systemHead.parent(); + let systemData = system.data(); systemHead.popover({ - placement: 'right', + placement: 'bottom', html: true, trigger: 'manual', container: mapElement, title: false, content: Util.getSystemRegionTable( - system.data('region'), - system.data('faction') || null + Util.getObjVal(systemData, 'region'), + Util.getObjVal(systemData, 'sovereignty') ) }); } diff --git a/js/app/map/scrollbar.js b/js/app/map/scrollbar.js index 6d983d43..3b3c73d5 100644 --- a/js/app/map/scrollbar.js +++ b/js/app/map/scrollbar.js @@ -1,73 +1,129 @@ -/** - * Created by Exodus on 26.06.2016. - */ define([ 'jquery', - 'app/init', - 'app/util', 'mousewheel', 'customScrollbar' -], ($, Init, Util) => { +], ($) => { 'use strict'; + let defaultConfig = { + axis: 'yx', + theme: 'light-3' , + scrollInertia: 200, + autoExpandScrollbar: false, + scrollButtons:{ + enable: true, + scrollAmount: 30, + scrollType: 'stepless' + }, + callbacks: { + onTotalScrollOffset: 0, + onTotalScrollBackOffset: 0, + alwaysTriggerOffsets: true + }, + + advanced: { + autoUpdateTimeout: 120, // auto-update timeout (default: 60) + updateOnContentResize: true, + autoExpandHorizontalScroll: false, // on resize css scale() scroll content should not change + //autoExpandHorizontalScroll: 2, + autoScrollOnFocus: 'div', + }, + mouseWheel: { + enable: false, // scroll wheel currently disabled + scrollAmount: 'auto', + axis: 'x', + preventDefault: true + }, + keyboard: { + enable: false, // not working with pathfinder "shortcuts" + scrollType: 'stepless', + scrollAmount: 'auto' + }, + scrollbarPosition: 'inside', + autoDraggerLength: true, + autoHideScrollbar: false + }; + /** * init map scrollbar + * @param scrollWrapper * @param config */ - $.fn.initCustomScrollbar = function(config){ - - // default config ------------------------------------------------------------------------- - let defaultConfig = { - axis: 'yx', - theme: 'light-3' , - scrollInertia: 300, - autoExpandScrollbar: false, - scrollButtons:{ - enable: true, - scrollAmount: 30, - scrollType: 'stepless' - }, - callbacks: { - onTotalScrollOffset: 0, - onTotalScrollBackOffset: 0, - alwaysTriggerOffsets: true - }, - - advanced: { - autoUpdateTimeout: 120, // auto-update timeout (default: 60) - updateOnContentResize: true, - autoExpandHorizontalScroll: false, // on resize css scale() scroll content should not change - //autoExpandHorizontalScroll: 2, - autoScrollOnFocus: 'div', - }, - mouseWheel: { - enable: false, // scroll wheel currently disabled - scrollAmount: 'auto', - axis: 'x', - preventDefault: true - }, - keyboard: { - enable: false, // not working with pathfinder "shortcuts" - scrollType: 'stepless', - scrollAmount: 'auto' - }, - scrollbarPosition: 'inside', - autoDraggerLength: true, - autoHideScrollbar: false - }; - - // init ----------------------------------------------------------------------------------- + let initScrollbar = (scrollWrapper, config) => { config = $.extend(true, {}, defaultConfig, config); - return this.each(function(){ - let mapWrapperElement = $(this); + scrollWrapper.mCustomScrollbar(config); + }; - // prevent multiple initialization - mapWrapperElement.mCustomScrollbar('destroy'); + /** + * get mCustomScrollbar container + * @param element + * @returns {*|[]} + */ + let getContainer = element => element.parents('.mCSB_container'); - // init custom scrollbars - mapWrapperElement.mCustomScrollbar(config); - }); + /** + * + * @param container + * @param element + * @returns {{x: number, y: number}} + */ + let getElementPos = (container, element) => { + return { + x: element.offset().left - container.offset().left, + y: element.offset().top - container.offset().top + }; + }; + + /** + * @param element + * @returns {{x: number, y: number}} + */ + let getElementDim = element => { + return { + x: element.outerWidth(false), + y: element.outerHeight(false) + }; + }; + + /** + * check if an element is 100% visible + * -> scrolled into viewport + * @param element + * @returns {boolean} + */ + let isInView = element => { + let container = getContainer(element); + let wrapper = container.parent(); + let cPos = {x: container[0].offsetLeft, y: container[0].offsetTop}; + let ePos = getElementPos(container, element); + let eDim = getElementDim(element); + + return cPos.y + ePos.y >= 0 && + cPos.y + ePos.y < wrapper.height() - eDim.y && + cPos.x + ePos.x >= 0 && + cPos.x + ePos.x < wrapper.width() - eDim.x; + }; + + /** + * get new scrollTo coordinates to center element in viewport + * @param element + * @returns {{x: number, y: number}} + */ + let getCenterScrollPosition = element => { + let container = getContainer(element); + let wrapper = container.parent(); + let cDim = getElementDim(container); + let wDim = {x: wrapper.width(), y: wrapper.height()}; + let eDim = getElementDim(element); + let ePos = getElementPos(container, element); + + let eOff = { + x: (-wDim.x / 2) + (eDim.x / 2), + y: (-wDim.y / 2) + (eDim.y / 2) + }; + + return adjustPos(addOffset(ePos, eOff), cDim); }; /** @@ -82,33 +138,55 @@ define([ }; /** - * scroll to a specific system on map + * scroll to center an element * -> subtract some offset for tooltips/connections * @param scrollWrapper - * @param position - * @param options + * @param element */ - let scrollToSystem = (scrollWrapper, position, options) => { - position = getOffsetPosition(position, {x: -15, y: -35}); - scrollToPosition(scrollWrapper, position, options); + let scrollToCenter = (scrollWrapper, element) => { + // no scroll if element is already FULL visible in scrollable viewport + if(!isInView(element)){ + // get scrollTo position for centered element + scrollToPosition(scrollWrapper, getCenterScrollPosition(element)); + } }; /** * add/subtract offset coordinates from position - * -> no negative values returned - * @param position - * @param offset + * @param {{x: number, y: number}} position + * @param {{x: number, y: number}} offset * @returns {{x: number, y: number}} */ - let getOffsetPosition = (position, offset) => { - return { - x: Math.max(0, position.x + offset.x), - y: Math.max(0, position.y + offset.y) - }; - }; + let addOffset = (position, offset) => mapObject(position, (v, k) => v + offset[k]); + + /** + * round position + * @param {{x: number, y: number}} position + * @returns {{x: number, y: number}} + */ + let roundPos = position => mapObject(position, Math.round); + + /** + * + * @param {{x: number, y: number}} position + * @param {{x: number, y: number}} dimension + * @returns {{x: number, y: number}} + */ + let adjustPos = (position, dimension) => mapObject(roundPos(position), (v, k) => Math.max(1, Math.min(dimension[k], v)) ); + + /** + * like Array.map() for objects + * -> callback f is called for each property + * @see https://stackoverflow.com/a/38829074/4329969 + * @param o + * @param f + * @returns {Object} + */ + let mapObject = (o, f) => Object.assign(...Object.entries(o).map(([k, v]) => ({[k]: f(v, k) }))); return { + initScrollbar: initScrollbar, scrollToPosition: scrollToPosition, - scrollToSystem: scrollToSystem + scrollToCenter: scrollToCenter }; }); \ No newline at end of file diff --git a/js/app/map/system.js b/js/app/map/system.js index beffcfc1..f04c9836 100644 --- a/js/app/map/system.js +++ b/js/app/map/system.js @@ -9,9 +9,8 @@ define([ 'app/util', 'bootbox', 'app/map/util', - 'app/map/layout', 'app/map/magnetizing' -], ($, Init, Util, bootbox, MapUtil, Layout, Magnetizer) => { +], ($, Init, Util, bootbox, MapUtil, Magnetizer) => { 'use strict'; let config = { @@ -245,7 +244,7 @@ define([ sourceSystem = options.sourceSystem; // get new position - newPosition = calculateNewSystemPosition(sourceSystem); + newPosition = newSystemPositionBySystem(sourceSystem); }else if(options.position){ // check mouse cursor position (add system to map) newPosition = { @@ -730,25 +729,16 @@ define([ }; /** - * calculate the x/y coordinates for a new system - relativ to a source system + * calculate the x/y coordinates for a new system - relative to a source system + * -> in case no coordinates found -> return default calculated coordinates * @param sourceSystem * @returns {{x: *, y: *}} */ - let calculateNewSystemPosition = sourceSystem => { - let mapContainer = sourceSystem.parent(); - let grid = [MapUtil.config.mapSnapToGridDimension, MapUtil.config.mapSnapToGridDimension]; + let newSystemPositionBySystem = sourceSystem => { let x = 0; let y = 0; - let positionFinder = new Layout.Position({ - container: mapContainer[0], - center: sourceSystem[0], - loops: 4, - grid: mapContainer.hasClass(MapUtil.config.mapGridClass) ? grid : false, - debug: false - }); - - let dimensions = positionFinder.findNonOverlappingDimensions(1, 16); + let dimensions = MapUtil.newSystemPositionBySystem(sourceSystem); if(dimensions.length){ //... empty map space found x = dimensions[0].left; @@ -780,8 +770,12 @@ define([ let headInfoLeft = []; let headInfoRight = []; + if(data.drifter){ + headInfoLeft.push(''); + } + if(data.shattered){ - headInfoLeft.push(''); + headInfoLeft.push(''); } // check systemData if headInfo element is needed diff --git a/js/app/map/util.js b/js/app/map/util.js index 9b19fe39..420ab0e4 100644 --- a/js/app/map/util.js +++ b/js/app/map/util.js @@ -6,9 +6,10 @@ define([ 'jquery', 'app/init', 'app/util', + 'app/map/layout', 'app/map/scrollbar', 'app/map/overlay/util' -], ($, Init, Util, Scrollbar, MapOverlayUtil) => { +], ($, Init, Util, Layout, Scrollbar, MapOverlayUtil) => { 'use strict'; let config = { @@ -806,10 +807,9 @@ define([ // collect all required data from map module to update the info element // store them global and assessable for each module - Util.setCurrentSystemData({ - systemData: system.getSystemData(), - mapId: parseInt( system.attr('data-mapid') ) - }); + let systemData = system.getSystemData(); + systemData.mapId = parseInt(system.attr('data-mapid')) || 0; + Util.setCurrentSystemData(systemData); }; /** @@ -1764,6 +1764,91 @@ define([ }); }; + /** + * add station services tooltip + * @param services + * @param options + * @returns {*} + */ + $.fn.addStationServiceTooltip = function(services, options){ + let getServiceIcon = service => { + switch(service){ + case 'bounty-missions': return false; + case 'assasination-missions': return false; + case 'courier-missions': return false; + case 'interbus': return false; + case 'reprocessing-plant': return 'reprocess'; + case 'refinery': return false; + case 'market': return 'market'; + case 'black-market': return false; + case 'stock-exchange': return false; + case 'cloning': return 'clonebay'; + case 'surgery': return false; + case 'dna-therapy': return false; + case 'repair-facilities': return 'repairshop'; + case 'factory': return 'industry'; + case 'labratory': return 'research'; + case 'gambling': return false; + case 'fitting': return 'fitting'; + case 'paintshop': return 'skins'; + case 'news': return false; + case 'storage': return false; + case 'insurance': return 'insurance'; + case 'docking': return 'docking'; + case 'office-rental': return false; + case 'jump-clone-facility': return 'jumpclones'; + case 'loyalty-point-store': return 'lpstore'; + case 'navy-offices': return 'factionalwarfare'; + case 'security-offices': return 'concord'; + default: return false; + } + }; + + let getStationServicesTable = services => { + let content = ''; + for(let i = 0; i < services.length; i++){ + let icon = getServiceIcon(services[i]); + if(icon){ + content += '' + services[i] + ''; + } + } + return content; + }; + + let content = getStationServicesTable(services); + + let title = ' Services'; + + let defaultOptions = { + placement: 'top', + html: true, + trigger: 'hover', + container: 'body', + title: title, + content: '', + delay: { + show: 150, + hide: 0 + }, + }; + + options = $.extend({}, defaultOptions, options); + + return this.each(function(){ + let element = $(this); + element.popover(options); + + // set new popover content + let popover = element.data('bs.popover'); + popover.options.content = content; + popover.tip().addClass(Util.config.popoverClass); + + if(options.show){ + element.popover('show'); + } + }); + }; + /** * add system effect tooltip * @param security @@ -1774,9 +1859,11 @@ define([ $.fn.addSystemEffectTooltip = function(security, effect, options){ let effectClass = getEffectInfoForSystem(effect, 'class'); let systemEffectData = Util.getSystemEffectData(security, effect); + let areaId = Util.getAreaIdBySecurity(security); let title = ' ' + - getEffectInfoForSystem(effect, 'name') + + getEffectInfoForSystem(effect, 'name') + '  ' + + '' + Util.getSystemEffectMultiplierByAreaId(parseInt(areaId)) + 'x' + '' + '' + security + ''; let content = Util.getSystemEffectTable(systemEffectData); @@ -1808,7 +1895,6 @@ define([ * @returns {*} */ $.fn.addSystemPlanetsTooltip = function(planets, options){ - let content = Util.getSystemPlanetsTable(planets); let defaultOptions = { @@ -1869,10 +1955,10 @@ define([ if(tooltipData.maxStableTime){ data.maxStableTime = tooltipData.maxStableTime + ' h'; } - if(tooltipData.signatureStrength){ - data.signatureStrength = parseFloat(tooltipData.signatureStrength).toLocaleString() + ' %'; + if(tooltipData.scanWormholeStrength){ + data.scanWormholeStrength = parseFloat(tooltipData.scanWormholeStrength).toLocaleString() + ' %'; }else{ - data.signatureStrength = 'unknown'; + data.scanWormholeStrength = 'unknown'; } let title = tooltipData.name; @@ -2029,6 +2115,132 @@ define([ return hasAccess; }; + /** + * + * @param options + * @param maxResults + * @param findChain + * @returns {Array} + */ + let findNonOverlappingDimensions = (options = {}, maxResults = 1, findChain = false) => { + let defaultOptions = { + center: [0, 30], + loops: 4, + debug: false + }; + + options = Object.assign({}, defaultOptions, options); + let positionFinder = new Layout.Position(Object.assign({}, defaultOptions, options)); + + return positionFinder.findNonOverlappingDimensions(maxResults, findChain); + }; + + /** + * calculate the x/y coordinates for a new system - relative to a source system + * @param sourceSystem + * @returns {Array} + */ + let newSystemPositionBySystem = sourceSystem => { + let mapContainer = sourceSystem.parent(); + let grid = [config.mapSnapToGridDimension, config.mapSnapToGridDimension]; + + let options = { + container: mapContainer[0], + center: sourceSystem[0], + grid: mapContainer.hasClass(config.mapGridClass) ? grid : false + }; + + return findNonOverlappingDimensions(options); + }; + + /** + * calculate the x/y coordinates for a new system - relative to x/y position + * @param mapContainer + * @param options + * @param maxResults + * @param findChain + * @returns {Array} + */ + let newSystemPositionByCoordinates = (mapContainer, options = {}, maxResults = 1, findChain = false) => { + let grid = [config.mapSnapToGridDimension, config.mapSnapToGridDimension]; + + let defaultOptions = { + container: mapContainer[0], + center: [0, 0], + grid: mapContainer.hasClass(config.mapGridClass) ? grid : false, + loops: 10, + defaultGapX: 10, + defaultGapY: 10, + //debugOk: true, + //debug: true + }; + + options = Object.assign({}, defaultOptions, options); + + return findNonOverlappingDimensions(options, maxResults, findChain); + }; + + /** + * + * @param mapContainer + */ + let newSystemPositionsByMap = mapContainer => { + let positions = {}; + + if(mapContainer){ + let mapId = mapContainer.data('id'); + let scrollPosition = { + x: Math.abs(parseInt(mapContainer.attr('data-scroll-left')) || 0), + y: Math.abs(parseInt(mapContainer.attr('data-scroll-top')) || 0) + }; + + // space new positions from map top (e.g. used for tooltips) + scrollPosition.y = Math.max(scrollPosition.y, 30); + + // default position -> current map section top/left ------------------------------------------------------- + positions.defaults = [scrollPosition]; + + // check default position for overlapping ----------------------------------------------------------------- + let dimensions = newSystemPositionByCoordinates(mapContainer, { + center: [scrollPosition.x, scrollPosition.y], + minX: scrollPosition.x, + minY: scrollPosition.y + }, 2, true); + + if(dimensions.length){ + positions.defaults = dimensions.map(dim => ({ + x: parseInt(dim.left) || 0, + y: parseInt(dim.top) || 0 + })); + } + + // -> calc possible coordinates for new system that should be used based on current user location --------- + let currentLocationData = Util.getCurrentLocationData(); + if(currentLocationData.id){ + // ... we need to the PF systemId for 'SelectSystem' trigger + let systemData = getSystemData(mapId, currentLocationData.id, 'systemId'); + if(systemData){ + let currentSystem = $('#' + getSystemId(mapId, systemData.id)); + if(currentSystem.length){ + let dimensions = newSystemPositionBySystem(currentSystem); + if(dimensions.length){ + //... empty map space found + positions.location = { + systemId: currentLocationData.id, + position: { + x: dimensions[0].left, + y: dimensions[0].top + } + }; + } + } + } + } + } + + return Object.keys(positions).length ? positions : null; + }; + /** * get a unique map url for deeplinking * @param mapId @@ -2108,6 +2320,9 @@ define([ initWormholeInfoTooltip: initWormholeInfoTooltip, getSystemId: getSystemId, checkRight: checkRight, + newSystemPositionBySystem: newSystemPositionBySystem, + newSystemPositionByCoordinates: newSystemPositionByCoordinates, + newSystemPositionsByMap: newSystemPositionsByMap, getMapDeeplinkUrl: getMapDeeplinkUrl }; }); \ No newline at end of file diff --git a/js/app/mappage.js b/js/app/mappage.js index 8c34ab2e..e129b841 100644 --- a/js/app/mappage.js +++ b/js/app/mappage.js @@ -9,10 +9,11 @@ define([ 'app/logging', 'app/page', 'app/map/worker', + 'app/map/util', 'app/module_map', 'app/key', 'app/ui/form_element' -], ($, Init, Util, Logging, Page, MapWorker, ModuleMap) => { +], ($, Init, Util, Logging, Page, MapWorker, MapUtil, ModuleMap) => { 'use strict'; @@ -31,6 +32,9 @@ define([ // set default dialog config Util.initDefaultBootboxConfig(); + // set default confirmation popover config + Util.initDefaultConfirmationConfig(); + // set default select2 config Util.initDefaultSelect2Config(); @@ -444,7 +448,7 @@ define([ // start user update trigger after map loaded updateTimeouts.userUpdate = setTimeout(() => { triggerUserUpdatePing(); - }, 1000); + }, 500); } }); } @@ -461,11 +465,13 @@ define([ // IMPORTANT: Get user data for ONE map that is currently visible // On later releases this can be easy changed to "full update" all maps for a user - // let mapIds = []; + let newSystemPositions = null; let activeMap = Util.getMapModule().getActiveMap(); + if(activeMap){ - mapIds = [ activeMap.data('id') ]; + mapIds = [activeMap.data('id')]; + newSystemPositions = MapUtil.newSystemPositionsByMap(activeMap); } let updatedUserData = { @@ -475,6 +481,10 @@ define([ systemData: Util.getCurrentSystemData() }; + if(newSystemPositions){ + updatedUserData.newSystemPositions = newSystemPositions; + } + Util.timeStart(logKeyServerUserData); $.ajax({ diff --git a/js/app/module_map.js b/js/app/module_map.js index 2863bf97..51bf9aa8 100644 --- a/js/app/module_map.js +++ b/js/app/module_map.js @@ -353,7 +353,7 @@ define([ // request "additional" system data (e.g. Structures, Description) // -> this is used to update some modules after initial draw - let promiseRequestData = Util.request('GET', 'system', currentSystemData.systemData.id, {mapId: currentSystemData.mapId}); + let promiseRequestData = Util.request('GET', 'system', currentSystemData.id, {mapId: currentSystemData.mapId}); // draw modules ------------------------------------------------------------------------------------------- @@ -361,22 +361,22 @@ define([ let secondCell = tabContentElement.find('.' + config.mapTabContentCellSecond); // draw system info module - let promiseInfo = drawModule(firstCell, SystemInfoModule, currentSystemData.mapId, currentSystemData.systemData); + let promiseInfo = drawModule(firstCell, SystemInfoModule, currentSystemData.mapId, currentSystemData); // draw system graph module - drawModule(firstCell, SystemGraphModule, currentSystemData.mapId, currentSystemData.systemData); + drawModule(firstCell, SystemGraphModule, currentSystemData.mapId, currentSystemData); // draw signature table module - let promiseSignature = drawModule(firstCell, SystemSignatureModule, currentSystemData.mapId, currentSystemData.systemData); + let promiseSignature = drawModule(firstCell, SystemSignatureModule, currentSystemData.mapId, currentSystemData); // draw system routes module - drawModule(secondCell, SystemRouteModule, currentSystemData.mapId, currentSystemData.systemData); + drawModule(secondCell, SystemRouteModule, currentSystemData.mapId, currentSystemData); // draw system intel module - let promiseIntel = drawModule(secondCell, SystemIntelModule, currentSystemData.mapId, currentSystemData.systemData); + let promiseIntel = drawModule(secondCell, SystemIntelModule, currentSystemData.mapId, currentSystemData); // draw system killboard module - drawModule(secondCell, SystemKillboardModule, currentSystemData.mapId, currentSystemData.systemData); + drawModule(secondCell, SystemKillboardModule, currentSystemData.mapId, currentSystemData); // update some modules ------------------------------------------------------------------------------------ promiseDrawAll.push(promiseRequestData, promiseInfo, promiseSignature, promiseIntel); @@ -479,7 +479,7 @@ define([ if( currentSystemData && - systemData.id === currentSystemData.systemData.id + systemData.id === currentSystemData.id ){ // trigger system update events let tabContentElement = $('#' + config.mapTabIdPrefix + systemData.mapId + '.' + config.mapTabContentClass); diff --git a/js/app/page.js b/js/app/page.js index ebc268b1..ef14e9cb 100644 --- a/js/app/page.js +++ b/js/app/page.js @@ -11,7 +11,6 @@ define([ 'app/map/util', 'app/map/contextmenu', 'slidebars', - 'text!img/logo.svg!strip', 'text!templates/layout/header_map.html', 'text!templates/layout/footer_map.html', 'dialog/notification', @@ -27,7 +26,7 @@ define([ 'dialog/credit', 'xEditable', 'app/module_map' -], ($, Init, Util, Logging, Mustache, MapUtil, MapContextMenu, SlideBars, TplLogo, TplHead, TplFooter) => { +], ($, Init, Util, Logging, Mustache, MapUtil, MapContextMenu, SlideBars, TplHead, TplFooter) => { 'use strict'; @@ -152,6 +151,7 @@ define([ loadFooter(pageElement), loadLeftMenu(pageMenuLeftElement), loadRightMenu(pageMenuRightElement), + loadSVGs() ]).then(payload => Promise.all([ setMenuObserver(payload[2].data), setMenuObserver(payload[3].data), @@ -413,6 +413,35 @@ define([ return new Promise(executor); }; + /** + * load standalone ´s into DOM + * -> SVGs can be used by referencing its ID e.g.: + * + * @returns {Promise} + */ + let loadSVGs = () => { + + let executor = resolve => { + let parentElement = $('body'); + + let svgPaths = [ + 'img/svg/logo.svg', + 'img/svg/swords.svg' + ].map(path => 'text!' + path + '!strip'); + + requirejs(svgPaths, (...SVGs) => { + parentElement.append.apply(parentElement, SVGs); + + resolve({ + action: 'loadSVGs', + data: {} + }); + }); + }; + + return new Promise(executor); + }; + /** * load page header * @param pageElement @@ -423,7 +452,6 @@ define([ let executor = resolve => { let moduleData = { id: config.pageHeaderId, - logo: () => Mustache.render(TplLogo, {}), brandLogo: config.menuHeadMenuLogoClass, popoverTriggerClass: Util.config.popoverTriggerClass, userCharacterClass: config.headUserCharacterClass, @@ -853,6 +881,10 @@ define([ let modalElement = $(e.target); modalElement.destroyTimestampCounter(true); + // destroy all form validators + // -> does not work properly. validation functions still used (js error) after 'destroy' + //modalElement.find('form').filter((i, form) => $(form).data('bs.validator')).validator('destroy'); + // destroy all popovers modalElement.find('.' + Util.config.popoverTriggerClass).popover('destroy'); @@ -980,7 +1012,13 @@ define([ if(changes.charactersIds){ updateTasks.push(updateHeaderCharacterSwitch(userData, changes.characterId)); } - if(changes.characterSystemId || changes.characterShipType || changes.characterLogHistory){ + if( + changes.characterSystemId || + changes.characterShipType || + changes.characterStationId || + changes.characterStructureId || + changes.characterLogHistory + ){ updateTasks.push(updateHeaderCharacterLocation(userData, changes.characterShipType)); } @@ -1022,7 +1060,7 @@ define([ 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'); + userInfoElement.find('img').attr('src', Util.eveImageUrl('character', Util.getObjVal(userData, 'character.id'))); } // init "character switch" popover userInfoElement.initCharacterSwitchPopover(userData); @@ -1055,6 +1093,16 @@ define([ let shipTypeId = Util.getObjVal(shipData, 'typeId') || 0; let shipTypeName = Util.getObjVal(shipData, 'typeName') || ''; + let stationData = Util.getObjVal(userData, 'character.log.station'); + let stationId = Util.getObjVal(stationData, 'id') || 0; + let stationName = Util.getObjVal(stationData, 'name') || ''; + + let structureData = Util.getObjVal(userData, 'character.log.structure'); + let structureTypeId = Util.getObjVal(structureData, 'type.id') || 0; + let structureTypeName = Util.getObjVal(structureData, 'type.name') || ''; + let structureId = Util.getObjVal(structureData, 'id') || 0; + let structureName = Util.getObjVal(structureData, 'name') || ''; + logDataAll.push(logData); // check for log history data as well @@ -1084,15 +1132,20 @@ define([ if(isCurrentLocation){ breadcrumbHtml += ''; + + if(stationId > 0){ + breadcrumbHtml += ''; + }else if(structureId > 0){ + breadcrumbHtml += ''; + } } breadcrumbHtml += systemName; if(isCurrentLocation && shipTypeId){ // show ship image - let shipSrc = Init.url.ccpImageServer + '/Render/' + shipTypeId + '_32.png'; breadcrumbHtml += ' { - let countElement = context.target.closest('.row').children().eq(1).find('kbd'); + let countElement = context.target.closest('tr').children().eq(1).find('kbd'); countElement.text(responseData.countBuildAll + '/' + responseData.countAll); countElement.removeClass('txt-color-success txt-color-danger txt-color-warning'); - if(responseData.countBuildAll >=responseData.countAll){ + if(responseData.countBuildAll >= responseData.countAll){ countElement.addClass('txt-color-success'); }else if(responseData.countBuildAll > 0){ countElement.addClass('txt-color-warning'); @@ -121,6 +128,12 @@ define([ countElement.addClass('txt-color-danger'); } + // update 'subCount' element (shows e.g. invType count) + if(responseData.subCount){ + let subCountElement = context.target.closest('tr').children().eq(2).find('kbd'); + subCountElement.text(responseData.subCount.countBuildAll + '/' + subCountElement.attr('data-countall')); + } + context.target.find('.btn-progress').html('  ' + responseData.progress + '%').css('width', responseData.progress + '%'); // send next chunk of rows -> import only @@ -130,6 +143,7 @@ define([ ){ sendRequest(context.url, { type: responseData.type, + countAll: responseData.countAll, count: responseData.count, offset: responseData.offset }, { @@ -188,8 +202,8 @@ define([ * @param data */ let updateWebSocketPanel = (data) => { - let badgeSocketWarning = $('.navbar a[data-anchor="#pf-setup-socket"] .txt-color-warning'); - let badgeSocketDanger = $('.navbar a[data-anchor="#pf-setup-socket"] .txt-color-danger'); + let badgeSocketWarning = $('.navbar a[data-target="pf-setup-socket"] .txt-color-warning'); + let badgeSocketDanger = $('.navbar a[data-target="pf-setup-socket"] .txt-color-danger'); let socketWarningCount = parseInt(badgeSocketWarning.text()) || 0; let socketDangerCount = parseInt(badgeSocketDanger.text()) || 0; diff --git a/js/app/summernote.loader.js b/js/app/summernote.loader.js index 636417af..c9f36b49 100644 --- a/js/app/summernote.loader.js +++ b/js/app/summernote.loader.js @@ -77,11 +77,9 @@ define([ // show "discard" changes confirmation let confirmationSettings = { - container: 'body', placement: 'top', - btnCancelClass: 'btn btn-sm btn-default', - btnCancelLabel: 'cancel', title: 'discard changes', + btnCancelIcon: '', btnOkClass: 'btn btn-sm btn-warning', btnOkLabel: 'discard', btnOkIcon: 'fas fa-fw fa-ban', diff --git a/js/app/ui/dialog/api_status.js b/js/app/ui/dialog/api_status.js index 97ab944c..2f48824a 100644 --- a/js/app/ui/dialog/api_status.js +++ b/js/app/ui/dialog/api_status.js @@ -38,7 +38,8 @@ define([ switch(render(val)){ case 'green': return 'ok'; case 'yellow': return 'degraded: Slow or potentially dropping requests'; - case 'red': return 'bad: Most requests are not succeeding and/or are very slow (5s+) on average'; + case 'orange': return 'bad: Most requests are not succeeding and/or are very slow (5s+) on average'; + case 'red': return 'error: Status data not available. Either offline or any other fatal error'; default: return 'unknown'; } }; diff --git a/js/app/ui/dialog/jump_info.js b/js/app/ui/dialog/jump_info.js index 39cc30e8..fae6407b 100644 --- a/js/app/ui/dialog/jump_info.js +++ b/js/app/ui/dialog/jump_info.js @@ -29,12 +29,39 @@ define([ */ $.fn.showJumpInfoDialog = function(){ requirejs(['text!templates/dialog/jump_info.html', 'mustache', 'datatables.loader'], (template, Mustache) => { + let iconShattered = ''; + let iconDrifter = ''; - let staticsMatrixHead = [ + let formatTableBodyData = (head, matrixBody) => { + return matrixBody.map((row, rowIndex) => { + return row.map((label, colIndex) => { + // get security name from "matrix Head" data if NOT first column + let secName = colIndex ? head[0][colIndex] : label; + return { + label: label, + class: Util.getSecurityClassForSystem(secName), + hasPopover: colIndex && label.length + }; + }); + }); + }; + + // Statics table first ------------------------------------------------------------------------------------ + let headGroupFirst = [ + [ + {label: '', class: 'separator-right', style: 'width: 55px;'}, + {colspan: 6, label: 'W-space', class: 'separator-right'}, + {colspan: 3, label: 'K-space', class: 'separator-right'}, + {label: 'Thera', class: 'separator-right'}, + {label: iconShattered} + ] + ]; + + let headFirst = [ ['From╲To', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'H', 'L', '0.0', 'C12', 'C13'] ]; - let staticsMatrixBody = [ + let matrixBodyFirst = [ ['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', ''], @@ -48,22 +75,48 @@ define([ ['?', 'E004', 'L005', 'Z006', 'M001', 'C008', 'G008', '' , '' , 'Q003', '' , 'A009'] ]; + + let staticsTableDataFirst = { + headGroup: headGroupFirst, + head: headFirst, + body: formatTableBodyData(headFirst, matrixBodyFirst) + }; + + // Statics table second ----------------------------------------------------------------------------------- + + let headGroupSecond = [ + [ + {label: '', class: 'separator-right', style: 'width: 55px;'}, + {label: iconDrifter + '  ' + 'Sentinel', class: 'separator-right'}, + {label: iconDrifter + '  ' + 'Barbican', class: 'separator-right'}, + {label: iconDrifter + '  ' + 'Vidette', class: 'separator-right'}, + {label: iconDrifter + '  ' + 'Conflux', class: 'separator-right'}, + {label: iconDrifter + '  ' + 'Redoubt'} + ] + ]; + + let headSecond = [ + ['From╲To', 'C14', 'C15', 'C16', 'C17', 'C18'] + ]; + + let matrixBodySecond = [ + ['?', 'S877', 'B735', 'V928', 'C414', 'R259'] + ]; + + let staticsTableDataSecond = { + headline: 'Drifter W-space', + headGroup: headGroupSecond, + head: headSecond, + body: formatTableBodyData(headSecond, matrixBodySecond) + }; + + let staticsTablesData = [staticsTableDataFirst, staticsTableDataSecond]; + let data = { config: config, popoverTriggerClass: Util.config.popoverTriggerClass, wormholes: Object.keys(Init.wormholes).map(function(k){ return Init.wormholes[k]; }), // convert Json to array - 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 - }; - }); - }), + staticsTablesData: staticsTablesData, massValue: function(){ return function(value, render){ let mass = render(value); diff --git a/js/app/ui/dialog/manual.js b/js/app/ui/dialog/manual.js index 62285ae7..cf0b8133 100644 --- a/js/app/ui/dialog/manual.js +++ b/js/app/ui/dialog/manual.js @@ -26,7 +26,6 @@ define([ $.fn.showMapManual = function(){ requirejs(['text!templates/dialog/map_manual.html', 'mustache'], (template, Mustache) => { - let data = { dialogNavigationClass: config.dialogNavigationClass, dialogNavLiClass: config.dialogNavigationListItemClass, @@ -67,8 +66,7 @@ define([ let scrollspyElement = $('#' + config.mapManualScrollspyId); - let whileScrolling = function(){ - + let whileScrolling = () => { if(disableOnScrollEvent === false){ for(let i = 0; i < scrollBreakpointElements.length; i++){ let offset = $(scrollBreakpointElements[i]).offset().top; @@ -124,11 +122,9 @@ define([ let mainNavigationLiElement = $(this).parent('.' + config.dialogNavigationListItemClass); - whileScrolling(); // if link is a main navigation link (not an anchor link) - if(mainNavigationLiElement.length > 0){ // remove all active classes scrollNavLiElements.removeClass('active'); @@ -138,7 +134,6 @@ define([ } }); - }, onScroll: function(){ disableOnScrollEvent = false; diff --git a/js/app/ui/dialog/map_info.js b/js/app/ui/dialog/map_info.js index 473110e7..ea5c7da7 100644 --- a/js/app/ui/dialog/map_info.js +++ b/js/app/ui/dialog/map_info.js @@ -56,18 +56,6 @@ define([ } }; - // confirmation dialog settings (e.g. delete row) - let confirmationSettings = { - container: 'body', - placement: 'left', - btnCancelClass: 'btn btn-sm btn-default', - btnCancelLabel: 'cancel', - btnCancelIcon: 'fas fa-fw fa-ban', - btnOkClass: 'btn btn-sm btn-danger', - btnOkLabel: 'delete', - btnOkIcon: 'fas fa-fw fa-times' - }; - /** * get icon that marks a table cell as clickable * @returns {string} @@ -217,7 +205,7 @@ define([ let systemsElement = $(this).empty(); let systemTable = $('', { - id: Util.getTableId(config.tableId, mapData.config.id, '', 'systems'), + id: Util.getTableId(config.tableId, 'systems', mapData.config.id, ''), class: ['compact', 'stripe', 'order-column', 'row-border'].join(' ') }); systemsElement.append(systemTable); @@ -237,7 +225,7 @@ define([ paging: true, lengthMenu: [[5, 10, 20, 50, -1], [5, 10, 20, 50, 'All']], ordering: true, - order: [14, 'desc'], + order: [15, 'desc'], hover: false, data: mapData.data.systems, columnDefs: [], @@ -285,7 +273,7 @@ define([ } },{ name: 'shattered', - title: '', + title: '', width: 10, className: ['text-center', 'min-screen-l'].join(' '), searchable: false, @@ -294,7 +282,7 @@ define([ display: (cellData, type, rowData, meta) => { let value = ''; if(cellData){ - value = ''; + value = ''; } return value; } @@ -324,6 +312,22 @@ define([ title: 'region', data: 'region.name', className: 'min-screen-l', + },{ + name: 'sovereignty', + title: 'sov.', + width: 30, + className: 'text-center', + data: 'sovereignty.alliance.ticker', + defaultContent: '', + render: { + display: (cellData, type, rowData, meta) => { + let value = ''; + if(cellData){ + value = '<' + cellData + '>'; + } + return value; + } + } },{ name: 'planets', title: '', @@ -456,44 +460,49 @@ define([ createdCell: function(cell, cellData, rowData, rowIndex, colIndex){ let tempTableElement = this; - let tempConfirmationSettings = confirmationSettings; - tempConfirmationSettings.title = 'Delete system'; - tempConfirmationSettings.onConfirm = function(e, target){ - let deleteRowElement = $(target).parents('tr'); + let confirmationSettings = { + placement: 'left', + title: 'Delete system', + template: Util.getConfirmationTemplate(null, { + size: 'small', + noTitle: true + }), + onConfirm: function(e, target){ + let deleteRowElement = $(target).parents('tr'); - let activeMap = Util.getMapModule().getActiveMap(); - let systemElement = $('#' + MapUtil.getSystemId(mapData.config.id, rowData.id) ); + let activeMap = Util.getMapModule().getActiveMap(); + let systemElement = $('#' + MapUtil.getSystemId(mapData.config.id, rowData.id) ); - if(systemElement.length){ - // trigger system delete event - activeMap.trigger('pf:deleteSystems', [{ - systems: [systemElement[0]], - callback: function(deletedSystems){ - // callback function after ajax "delete" success - // check if system was deleted - if(deletedSystems.length === 1){ - // remove table row - tempTableElement.DataTable().rows(deleteRowElement).remove().draw(); + if(systemElement.length){ + // trigger system delete event + activeMap.trigger('pf:deleteSystems', [{ + systems: [systemElement[0]], + callback: function(deletedSystems){ + // callback function after ajax "delete" success + // check if system was deleted + if(deletedSystems.length === 1){ + // remove table row + tempTableElement.DataTable().rows(deleteRowElement).remove().draw(); - Util.showNotify({title: 'System deleted', text: rowData.name, type: 'success'}); + Util.showNotify({title: 'System deleted', text: rowData.name, type: 'success'}); - // refresh connection table (connections might have changed) -------------- - let connectionsElement = $('#' + config.mapInfoConnectionsId); - let mapDataNew = activeMap.getMapDataFromClient(['hasId']); + // refresh connection table (connections might have changed) -------------- + let connectionsElement = $('#' + config.mapInfoConnectionsId); + let mapDataNew = activeMap.getMapDataFromClient(['hasId']); - connectionsElement.initConnectionInfoTable(mapDataNew); - }else{ - // error - Util.showNotify({title: 'Failed to delete system', text: rowData.name, type: 'error'}); + connectionsElement.initConnectionInfoTable(mapDataNew); + }else{ + // error + Util.showNotify({title: 'Failed to delete system', text: rowData.name, type: 'error'}); + } } - } - }]); + }]); + } } }; // init confirmation dialog - $(cell).confirmation(tempConfirmationSettings); - + $(cell).confirmation(confirmationSettings); } } ], @@ -652,23 +661,29 @@ define([ createdCell: function(cell, cellData, rowData, rowIndex, colIndex){ let tempTableElement = this; - let tempConfirmationSettings = confirmationSettings; - tempConfirmationSettings.title = 'Delete connection'; - tempConfirmationSettings.onConfirm = function(e, target){ - let deleteRowElement = $(target).parents('tr'); + let confirmationSettings = { + placement: 'left', + title: 'Delete connection', + template: Util.getConfirmationTemplate(null, { + size: 'small', + noTitle: true + }), + onConfirm: function(e, target){ + let deleteRowElement = $(target).parents('tr'); - // deleteSignatures(row); - let connection = $().getConnectionById(mapData.config.id, rowData.id); + // deleteSignatures(row); + let connection = $().getConnectionById(mapData.config.id, rowData.id); - MapUtil.deleteConnections([connection], () => { - // callback function after ajax "delete" success - // remove table row - tempTableElement.DataTable().rows(deleteRowElement).remove().draw(); - }); + MapUtil.deleteConnections([connection], () => { + // callback function after ajax "delete" success + // remove table row + tempTableElement.DataTable().rows(deleteRowElement).remove().draw(); + }); + } }; // init confirmation dialog - $(cell).confirmation(tempConfirmationSettings); + $(cell).confirmation(confirmationSettings); } } ], @@ -749,7 +764,7 @@ define([ _: function(data, type, row, meta){ let value = data; if(data && type === 'display'){ - value = ''; + value = ''; } return value; } @@ -787,7 +802,7 @@ define([ _: function(data, type, row, meta){ let value = data; if(type === 'display'){ - value = ''; + value = ''; } return value; } @@ -827,7 +842,7 @@ define([ _: function(data, type, row, meta){ let value = data; if(type === 'display'){ - value = ''; + value = ''; } return value; } @@ -849,7 +864,7 @@ define([ } }, createdCell: function(cell, cellData, rowData, rowIndex, colIndex){ - // open character information window (ingame) + // open corporation information window (ingame) $(cell).on('click', { tableApi: this.api() }, function(e){ let cellData = e.data.tableApi.cell(this).data(); Util.openIngameWindow(cellData.id); @@ -1110,7 +1125,7 @@ define([ _: function(data, type, row, meta){ let value = data; if(type === 'display'){ - value = ''; + value = ''; } return value; } diff --git a/js/app/ui/dialog/map_settings.js b/js/app/ui/dialog/map_settings.js index 4e628d5c..56389ea0 100644 --- a/js/app/ui/dialog/map_settings.js +++ b/js/app/ui/dialog/map_settings.js @@ -36,6 +36,7 @@ define([ 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 + trackAbyssalJumpsId: 'pf-map-dialog-track-abyss-jumps', // id for "trackAbyssalJumps" checkbox logHistoryId: 'pf-map-dialog-history', // id for "history logging" checkbox logActivityId: 'pf-map-dialog-activity', // id for "activity" checkbox @@ -159,6 +160,7 @@ define([ let deleteEolConnections = true; let persistentAliases = true; let persistentSignatures = true; + let trackAbyssalJumps = true; let logActivity = true; let logHistory = true; @@ -197,6 +199,7 @@ define([ deleteEolConnections = mapData.config.deleteEolConnections; persistentAliases = mapData.config.persistentAliases; persistentSignatures = mapData.config.persistentSignatures; + trackAbyssalJumps = mapData.config.trackAbyssalJumps; logActivity = mapData.config.logging.activity; logHistory = mapData.config.logging.history; @@ -255,10 +258,12 @@ define([ deleteEolConnectionsId : config.deleteEolConnectionsId, persistentAliasesId : config.persistentAliasesId, persistentSignaturesId : config.persistentSignaturesId, + trackAbyssalJumpsId : config.trackAbyssalJumpsId, deleteExpiredConnections: deleteExpiredConnections, deleteEolConnections: deleteEolConnections, persistentAliases: persistentAliases, persistentSignatures: persistentSignatures, + trackAbyssalJumps: trackAbyssalJumps, logHistoryId: config.logHistoryId, logActivityId: config.logActivityId, @@ -398,6 +403,9 @@ define([ if( form.find('#' + config.persistentSignaturesId).length ){ formData.persistentSignatures = formData.hasOwnProperty('persistentSignatures') ? parseInt( formData.persistentSignatures ) : 0; } + if( form.find('#' + config.trackAbyssalJumpsId).length ){ + formData.trackAbyssalJumps = formData.hasOwnProperty('trackAbyssalJumps') ? parseInt( formData.trackAbyssalJumps ) : 0; + } if( form.find('#' + config.logHistoryId).length ){ formData.logHistory = formData.hasOwnProperty('logHistory') ? parseInt( formData.logHistory ) : 0; } diff --git a/js/app/ui/dialog/stats.js b/js/app/ui/dialog/stats.js index b136109d..034f2ced 100644 --- a/js/app/ui/dialog/stats.js +++ b/js/app/ui/dialog/stats.js @@ -130,7 +130,7 @@ define([ data: 'character', render: { _: function(data, type, row, meta){ - return ''; + return ''; } } },{ diff --git a/js/app/ui/dialog/system_effects.js b/js/app/ui/dialog/system_effects.js index 8d2b03a5..df2ef98d 100644 --- a/js/app/ui/dialog/system_effects.js +++ b/js/app/ui/dialog/system_effects.js @@ -34,8 +34,8 @@ define([ let systemEffectData = Util.getSystemEffectData(); - // last active (hover) table columnIndex - let lastActiveColIndex = null; + // last active (hover) table columnName + let lastActiveColName = null; let colCount = 0; for(let [effectName, effectData] of Object.entries(systemEffectData.wh)){ @@ -59,8 +59,10 @@ define([ let securityClass = Util.getSecurityClassForSystem(systemType); if(areaId === '1'){ + rows.push( $('') ); rows.push( $('') ); thead.append( rows[0] ); + thead.append( rows[1] ); rows[0].append( $('') ); - tbody.append(rows[i + 1]); + tbody.append(rows[i + 2]); // add label - rows[i + 1].append( $(' DOM id by id * @param tableApi * @param id * @returns {*} */ - let getRowId = (tableApi, id) => { - return tableApi.rows().ids().toArray().find(rowId => rowId === config.tableRowIdPrefix + id); + let getRowById = (tableApi, id) => { + return tableApi.rows().ids().toArray().find(rowId => rowId === getRowId(Util.getObjVal(getTableMetaData(tableApi), 'type'), id)); }; /** - * callback -> add structure rows from systemData - * @param context - * @param systemData + * get custom "metaData" from dataTables API + * @param tableApi + * @returns {*} */ - let callbackUpdateStructureRows = (context, systemData) => { + let getTableMetaData = tableApi => { + let data = null; + if(tableApi){ + data = tableApi.init().pfMeta; + } + return data; + }; + + /** + * vormat roman numeric string to int + * -> e.g. 'VII' => 7 + * @param str + * @returns {number} + */ + let romanToInt = str => { + let charToTnt = char => { + switch (char) { + case 'I': return 1; + case 'V': return 5; + case 'X': return 10; + case 'L': return 50; + case 'C': return 100; + case 'D': return 500; + case 'M': return 1000; + default: return -1; + } + }; + + if(str == null) return -1; + let num = charToTnt(str.charAt(0)); + let pre, curr; + + for(let i = 1; i < str.length; i++){ + curr = charToTnt(str.charAt(i)); + pre = charToTnt(str.charAt(i - 1)); + if(curr <= pre){ + num += curr; + }else{ + num = num - pre * 2 + curr; + } + } + + return num; + }; + + /** + * callback -> add table rows from grouped tableData + * @param context + * @param tableData + * @param groupedDataKey + */ + let callbackUpdateTableRows = (context, tableData, groupedDataKey = 'structures') => { let touchedRows = []; let hadData = context.tableApi.rows().any(); let notificationCounter = { @@ -81,41 +163,39 @@ define([ deleted: 0 }; - if(systemData){ - let corporations = Util.getObjVal(systemData, 'structures'); - if(corporations){ - for(let [corporationId, corporationData] of Object.entries(corporations)){ - if(corporationData.structures && corporationData.structures.length){ - for(let structureData of corporationData.structures){ - let rowId = getRowId(context.tableApi, structureData.id); + if(tableData){ + for(let [rowGroupId, rowGroupData] of Object.entries(tableData)){ + if(rowGroupData[groupedDataKey] && rowGroupData[groupedDataKey].length){ + for(let rowData of rowGroupData[groupedDataKey]){ + let rowId = getRowById(context.tableApi, rowData.id); - // add corporation data - structureData.corporation = { - id: corporationData.id, - name: corporationData.name - }; + // add rowGroupData as well to each rowData + rowData.rowGroupData = { + id: rowGroupData.id, + name: rowGroupData.name, + groupedDataKey: groupedDataKey + }; - if(rowId){ - // update row - let api = context.tableApi.row('#' + rowId); - let rowData = api.data(); + if(rowId){ + // update row + let api = context.tableApi.row('#' + rowId); + let rowDataCurrent = api.data(); - // check for update - if(rowData.updated.updated !== structureData.updated.updated){ - // row data changed -> update - api.data(structureData); - notificationCounter.changed++; - } - - touchedRows.push(api.id()); - }else{ - // insert new row - let api = context.tableApi.row.add(structureData); - api.nodes().to$().data('animationStatus', 'added'); - notificationCounter.added++; - - touchedRows.push(api.id()); + // check for update + if(rowDataCurrent.updated.updated !== rowData.updated.updated){ + // row data changed -> update + api.data(rowData); + notificationCounter.changed++; } + + touchedRows.push(api.id()); + }else{ + // insert new row + let api = context.tableApi.row.add(rowData); + api.nodes().to$().data('animationStatus', 'added'); + notificationCounter.added++; + + touchedRows.push(api.id()); } } } @@ -156,7 +236,7 @@ define([ let deletedCounter = 0; if(structureIds && structureIds.length){ for(let structureId of structureIds){ - let rowId = getRowId(context.tableApi, structureId); + let rowId = getRowById(context.tableApi, structureId); if(rowId){ context.tableApi.row('#' + rowId).remove(); deletedCounter++; @@ -214,10 +294,36 @@ define([ return data; }); + // if current user is currently docked at a structure (not station) + // -> add a modal button for pre-fill modal with it + // -> systemId must match systemId from current character log + let currentUserData = Util.getCurrentUserData(); + let isCurrentLocation = false; + let characterStructureId = Util.getObjVal(currentUserData, 'character.log.structure.id') || 0; + let characterStructureName = Util.getObjVal(currentUserData, 'character.log.structure.name') || ''; + let characterStructureTypeId = Util.getObjVal(currentUserData, 'character.log.structure.type.id') || 0; + let characterStructureTypeName = Util.getObjVal(currentUserData, 'character.log.structure.type.name') || ''; + + if(systemId === Util.getObjVal(currentUserData, 'character.log.system.id')){ + isCurrentLocation = true; + } + + let disableButtonAutoFill = true; + let buttonLabelAutoFill = ' '; + if(characterStructureId){ + buttonLabelAutoFill += characterStructureTypeName + ' "' + characterStructureName + '"'; + if(isCurrentLocation){ + disableButtonAutoFill = false; + } + }else{ + buttonLabelAutoFill += 'unknown structure'; + } + let data = { id: config.structureDialogId, structureData: structureData, structureStatus: statusData, + nameInputId: config.nameInputId, statusSelectId: config.statusSelectId, typeSelectId: config.typeSelectId, corporationSelectId: config.corporationSelectId, @@ -236,7 +342,18 @@ define([ buttons: { close: { label: 'cancel', - className: 'btn-default' + className: 'btn-default pull-left' + }, + autoFill: { + label: buttonLabelAutoFill, + className: 'btn-primary' + (disableButtonAutoFill ? ' pf-font-italic disabled' : ''), + callback: function(){ + let form = this.find('form'); + form.find('#' + config.nameInputId).val(characterStructureName); + form.find('#' + config.statusSelectId).val(2).trigger('change'); + form.find('#' + config.typeSelectId).val(characterStructureTypeId).trigger('change'); + return false; + } }, success: { label: ' save', @@ -268,7 +385,7 @@ define([ }, context => context.moduleElement.hideLoadingAnimation() ).then( - payload => callbackUpdateStructureRows(payload.context, {structures: payload.data}), + payload => callbackUpdateTableRows(payload.context, payload.data), Util.handleAjaxErrorResponse ); }else{ @@ -361,6 +478,83 @@ define([ }); }; + /** + * init station services tooltips + * @param element + * @param tableApi + */ + let initStationServiceTooltip = (element, tableApi) => { + element.hoverIntent({ + over: function(e){ + let cellElement = $(this); + let rowData = tableApi.row(cellElement.parents('tr')).data(); + cellElement.addStationServiceTooltip(Util.getObjVal(rowData, 'services'), { + placement: 'left', + trigger: 'manual', + show: true + }); + }, + out: function(e){ + $(this).destroyPopover(); + }, + selector: 'td.' + config.tableCellServicesClass + }); + }; + + /** + * get dataTables default options for intel tables + * @returns {*} + */ + let getDataTableDefaults = () => { + return { + paging: false, + lengthChange: false, + ordering: true, + info: false, + searching: false, + hover: false, + autoWidth: false, + drawCallback: function (settings) { + let tableApi = this.api(); + let columnCount = tableApi.columns(':visible').count(); + let rows = tableApi.rows({page: 'current'}).nodes(); + let last = null; + + tableApi.column('rowGroupData:name', {page: 'current'}).data().each(function (group, i) { + if (!last || last.id !== group.id) { + // "stations" are grouped by "raceId" with its "factionId" + // "structures" are grouped by "corporationId" that ADDED it (not the ingame "owner" of it) + let imgType = 'stations' === group.groupedDataKey ? 'alliance' : 'corporation'; + + $(rows).eq(i).before( + '' + + '' + + '' + + '' + + '' + ); + last = group; + } + }); + + let animationRows = rows.to$().filter(function () { + return ( + $(this).data('animationStatus') || + $(this).data('animationTimer') + ); + }); + + for (let i = 0; i < animationRows.length; i++) { + let animationRow = $(animationRows[i]); + animationRow.pulseBackgroundColor(animationRow.data('animationStatus')); + animationRow.removeData('animationStatus'); + } + } + }; + }; + /** * get module element * @param parentElement @@ -369,6 +563,7 @@ define([ * @returns {jQuery} */ let getModule = (parentElement, mapId, systemData) => { + let showStationTable = ['H', 'L', '0.0', 'C12'].includes(Util.getObjVal(systemData, 'security')); let corporationId = Util.getCurrentUserInfo('corporationId'); let moduleElement = $('
').append( @@ -395,35 +590,29 @@ define([ }).attr('data-html', 'true').attr('data-toggle', 'tooltip') ), $('
', { - text: 'Intel' + text: 'Structures' }) ) ); - let table = $('
').html('  ' + systemEffectName).prepend( @@ -69,24 +71,30 @@ define([ }) ) ); + + rows[1].append($('')); } rows[0].append( $('', { class: ['text-right', 'col-xs-1', securityClass].join(' ') - }).text( systemType )); + }).text( systemType ).attr('data-name', systemType)); + + rows[1].append( $('', { + class: ['text-right', 'txt-color', 'txt-color-grayLight'].join(' ') + }).text(Util.getSystemEffectMultiplierByAreaId(parseInt(areaId)) + ' x').attr('data-name', systemType)); for(let [i, data] of Object.entries(areaData)){ i = parseInt(i); if(areaId === '1'){ rows.push( $('
').text( data.effect )); + rows[i + 2].append( $('').text( data.effect )); } - rows[i + 1].append( $('', { + rows[i + 2].append( $('', { class: 'text-right' }).text( data.value )); } @@ -148,25 +156,26 @@ define([ tableApi.tables().nodes().to$().on('mouseover', 'td', function(){ // inside table cell -> get current hover colIndex let colIndex = tableApi.cell(this).index().column; + let colName = tableApi.column(colIndex).header().dataset.name || ''; - if(colIndex !== lastActiveColIndex){ + if(colName !== lastActiveColName){ removeColumnHighlight(); - lastActiveColIndex = colIndex; + lastActiveColName = colName; - if(colIndex > 0){ - // active column changed -> highlight same colIndex on other tables + if(colName.length){ + // active column changed -> highlight same colName on other tables let tableApis = $.fn.dataTable.tables({ visible: false, api: true }) .tables('.' + config.systemEffectTableClass); - let columns = tableApis.columns(colIndex); + let columns = tableApis.columns([colName + ':name']); columns.header().flatten().to$().addClass('colHighlight'); columns.nodes().flatten().to$().addClass('colHighlight'); } } }).on('mouseleave', function(){ // no longer inside table - lastActiveColIndex = null; + lastActiveColName = null; removeColumnHighlight(); }); } diff --git a/js/app/ui/form_element.js b/js/app/ui/form_element.js index 312b837b..6474ccba 100644 --- a/js/app/ui/form_element.js +++ b/js/app/ui/form_element.js @@ -38,19 +38,19 @@ define([ switch(data.categoryType){ case 'character': - imagePath = Init.url.ccpImageServer + '/Character/' + data.id + '_32.jpg'; + imagePath = Util.eveImageUrl('character', data.id); break; case 'corporation': - imagePath = Init.url.ccpImageServer + '/Corporation/' + data.id + '_32.png'; + imagePath = Util.eveImageUrl('corporation', data.id); break; case 'alliance': - imagePath = Init.url.ccpImageServer + '/Alliance/' + data.id + '_32.png'; + imagePath = Util.eveImageUrl('alliance', data.id); break; case 'inventoryType': - imagePath = Init.url.ccpImageServer + '/Type/' + data.id + '_32.png'; + imagePath = Util.eveImageUrl('type', data.id); break; case 'render': - imagePath = Init.url.ccpImageServer + '/Render/' + data.id + '_32.png'; + imagePath = Util.eveImageUrl('render', data.id); break; case 'station': iconName = 'fa-home'; @@ -86,10 +86,19 @@ define([ if(parts.length === 2){ // wormhole data -> 2 columns let securityClass = Util.getSecurityClassForSystem(getSystemSecurityFromLabel(parts[1])); + // some labels have a "suffix" label that should not have the securityClass + let labelParts = parts[1].split(/\s(.+)/); + let label = labelParts[0]; + let suffix = labelParts[1] ? labelParts[1] : ''; + + let classes = [securityClass, Util.config.popoverTriggerClass, Util.config.helpDefaultClass]; + markup += '' + parts[0] + '  '; markup += ''; - markup += '  ' + parts[1] + ' '; + markup += '  ' + label + ''; + if(suffix.length){ + markup += ' ' + suffix + ''; + } }else{ markup += '' + state.text + ''; } @@ -122,9 +131,16 @@ define([ switch(formatType){ case 'wormhole': + // some labels have a "suffix" label that should not have the securityClass + let labelParts = parts[1].split(/\s(.+)/); + let label = labelParts[0]; + let suffix = labelParts[1] ? labelParts[1] : ''; + + markup += '
' + parts[0] + '
'; markup += '
'; - markup += '
' + parts[1] + '
'; + markup += '
' + label + '
'; + markup += '
' + suffix + '
'; break; case 'system': markup += '
' + parts[0] + '
'; @@ -333,7 +349,7 @@ define([ markup += ''; markup += '
' + data.security + '
'; markup += '
'; - markup += ''; + markup += ''; markup += '
'; markup += '
' + data.trueSec + '
'; diff --git a/js/app/ui/module/connection_info.js b/js/app/ui/module/connection_info.js index b165155a..8c86e5e3 100644 --- a/js/app/ui/module/connection_info.js +++ b/js/app/ui/module/connection_info.js @@ -189,7 +189,7 @@ define([ html: '' }), $('
', { - text: scopeLabel.charAt(0).toUpperCase() + scopeLabel.slice(1) + text: scopeLabel.capitalize() }), $('', { class: ['text-right', config.connectionInfoTableCellConnectionClass].join(' ') @@ -774,7 +774,7 @@ define([ _: function(data, type, row){ let value = data.typeId; if(type === 'display'){ - value = ''; + value = ''; } return value; } @@ -794,7 +794,7 @@ define([ _: (cellData, type, rowData, meta) => { let value = cellData.name; if(type === 'display'){ - value = ''; + value = ''; } return value; } @@ -883,15 +883,11 @@ define([ if(rowData.active){ let confirmationSettings = { - container: 'body', - placement: 'left', - btnCancelClass: 'btn btn-sm btn-default', - btnCancelLabel: 'cancel', - btnCancelIcon: 'fas fa-fw fa-ban', title: 'delete jump log', - btnOkClass: 'btn btn-sm btn-danger', - btnOkLabel: 'delete', - btnOkIcon: 'fas fa-fw fa-times', + template: Util.getConfirmationTemplate(null, { + size: 'small', + noTitle: true + }), onConfirm : function(e, target){ // get current row data (important!) // -> "rowData" param is not current state, values are "on createCell()" state diff --git a/js/app/ui/module/system_graph.js b/js/app/ui/module/system_graph.js index dafe1806..c6d67952 100644 --- a/js/app/ui/module/system_graph.js +++ b/js/app/ui/module/system_graph.js @@ -265,7 +265,7 @@ define([ for(let [graphKey, graphConfig] of Object.entries(config.systemGraphs)){ rowElement.append( $('
', { - class: ['col-xs-12', 'col-sm-6', 'col-md-4'].join(' ') + class: ['col-xs-12', 'col-sm-4'].join(' ') }).append( $('
', { class: config.moduleHeadClass diff --git a/js/app/ui/module/system_info.js b/js/app/ui/module/system_info.js index 5136c3bb..e9d01141 100644 --- a/js/app/ui/module/system_info.js +++ b/js/app/ui/module/system_info.js @@ -27,7 +27,8 @@ define([ typeLinkClass: 'pf-system-info-type', // class for "type" name urlLinkClass: 'pf-system-info-url', // class for "url" copy link - // info table + // info col/table + systemInfoSectionClass: 'pf-system-info-section', // class for system info section systemInfoTableClass: 'pf-module-table', // class for system info table systemInfoNameClass: 'pf-system-info-name', // class for "name" information element systemInfoEffectClass: 'pf-system-info-effect', // class for "effect" information element @@ -37,9 +38,21 @@ define([ systemInfoWormholeClass: 'pf-system-info-wormhole-', // class prefix for static wormhole element // description field - descriptionAreaClass: 'pf-system-info-description-area', // class for "description" area + descriptionSectionClass: 'pf-system-description-section', // class for system description section + descriptionAreaClass: 'pf-system-info-description-area', // class for description area addDescriptionButtonClass: 'pf-system-info-description-button', // class for "add description" button - descriptionTextareaElementClass: 'pf-system-info-description', // class for "description" textarea element (Summernote) + descriptionTextareaElementClass: 'pf-system-info-description', // class for description textarea element (Summernote) + + // sovereignty col/table + systemSovSectionClass: 'pf-system-sov-section', // class for system sov. section + systemSovTableClass: 'pf-module-table', // class for system sov. table + systemSovFwContestedRowClass: 'pf-system-sov-fw-contested-row', // class for "contested" sov. table row + systemSovFwOccupationRowClass: 'pf-system-sov-fw-occupation-row', // class for "-occupation" sov. table row + systemSovFwContestedClass: 'pf-system-sov-fw-contested', + systemSovFwPercentageClass: 'pf-system-sov-fw-percentage', + systemSovFwOccupationClass: 'pf-system-sov-fw-occupation', + systemSovFwOccupationImageClass: 'pf-system-sov-fw-occupation-image', + systemSovFwStatusIconClass: 'pf-system-sov-fw-status-icon', // fonts fontTriglivianClass: 'pf-triglivian', // class for "Triglivian" names (e.g. Abyssal systems) @@ -115,12 +128,119 @@ define([ } } + // update faction warfare rows ---------------------------------------------------------------------------- + let fwContestedRow = moduleElement.find('.' + config.systemSovFwContestedRowClass); + let fwOccupationRow = moduleElement.find('.' + config.systemSovFwOccupationRowClass); + if(systemData.factionWar){ + let contested = String(Util.getObjVal(systemData.factionWar, 'contested') || ''); + let percentage = parseInt(Util.getObjVal(systemData.factionWar, 'victoryPercentage')) || 0; + let occupierFaction = Util.getObjVal(systemData.factionWar, 'occupierFaction'); + + let statusColor = 'red'; + if(occupierFaction){ + // system is "occupied" by hostile "occupierFaction" (stable) + // -> hide percent + statusColor = '#d9534f'; + percentage += '%'; + }else if('uncontested' === contested){ + // system is "uncontested" and owned by default ownerFaction (stable) + // -> hide percent + statusColor = '#4f9e4f'; + percentage = 'stable'; + }else if('contested' === contested){ + // system is "contested", 0%-99% percentage + statusColor = '#e28a0d'; + percentage += '%'; + }else if( + 'vulnerable' === contested || + 'captured' === contested + ){ + // system is "vulnerable", 100% percentage + // -> "captured" state is might be the same?! + statusColor = '#d747d6'; + percentage = '100%'; + } + + fwContestedRow.find('.' + config.systemSovFwStatusIconClass)[0].style.setProperty('--color', statusColor); + fwContestedRow.find('.' + config.systemSovFwContestedClass).text(contested); + fwContestedRow.find('.' + config.systemSovFwPercentageClass).text(percentage); + fwContestedRow.show(); + + let occupierFactionImage = Util.eveImageUrl('alliance', (occupierFaction ? occupierFaction.id : 0), 64); + let occupierFactionName = occupierFaction ? occupierFaction.name : ''; + + fwOccupationRow.find('.' + config.systemSovFwOccupationImageClass)[0].style.setProperty('--bg-image', 'url(\'' + occupierFactionImage + '\')'); + fwOccupationRow.find('.' + config.systemSovFwOccupationClass).text(occupierFactionName); + if(occupierFaction){ + fwOccupationRow.show(); + } + }else{ + fwContestedRow.hide(); + fwOccupationRow.hide(); + } + if(setUpdated){ moduleElement.data('updated', systemData.updated.updated); } } moduleElement.find('.' + config.descriptionAreaClass).hideLoadingAnimation(); + moduleElement.find('.' + config.systemSovSectionClass + ' .' + Util.config.dynamicAreaClass).hideLoadingAnimation(); + }; + + /** + * @param pages + * @param systemData + */ + let getThirdPartySystemLinks = (pages, systemData) => { + let urls = {}; + let isWormhole = MapUtil.getSystemTypeInfo(Util.getObjVal(systemData, 'type.id'), 'name') === 'w-space'; + let systemName = Util.getObjVal(systemData, 'name') || ''; + let regionName = Util.getObjVal(systemData, 'region.name') || ''; + + let validUrls = 0; + for(let i = 0; i < pages.length; i++){ + let url = false; + let domain = Util.getObjVal(Init, 'url.' + pages[i]); + if(domain || pages[i] === 'eve'){ + switch(pages[i]){ + case 'eve': + url = 'https://client'; // fake url + break; + case 'dotlan': + let systemNameTemp = systemName.replace(/ /g, '_'); + let regionNameTemp = regionName.replace(/ /g, '_'); + if(isWormhole){ + url = domain + '/system/' + systemNameTemp; + }else{ + url = domain + '/map/' + regionNameTemp + '/' + systemNameTemp; + } + break; + case 'eveeye': + if(!isWormhole){ + url = domain + '/?m=' + encodeURIComponent(regionName) + '&s=' + encodeURIComponent(systemName); + url += '&t=eswkc&o=thera,con_svc,node_sov,sub_sec,sector_fac,tag_mk'; + } + break; + case 'anoik': + if(isWormhole){ + url = domain + '/systems/' + systemName; + } + break; + } + + if(url){ + let urlObj = new URL(url); + urls[++validUrls + '_url'] = { + page: pages[i], + domain: urlObj.hostname, + url: url + }; + } + } + } + + return urls; }; /** @@ -135,6 +255,56 @@ define([ // store systemId -> module can be updated with the correct data moduleElement.data('id', systemData.id); + // system "sovereignty" data + // "primary" data is eigther "alliance" -> 0.0 space + // or "faction" -> Empire Regions (LS, HS) + let sovereigntyDefault = { + row1Label: 'Sov.', + row1Val: '???', + row1Img: undefined, + row1ImgTitle: undefined, + row2Label: undefined, + row2Val: undefined, + row3Label: undefined, + row3Val: undefined + }; + + let sovereigntyPrimary; + let sovereigntySecondary; + + if(systemData.sovereignty){ + let sovDataFact = Util.getObjVal(systemData.sovereignty, 'faction'); + let sovDataAlly = Util.getObjVal(systemData.sovereignty, 'alliance'); + let sovDataCorp = Util.getObjVal(systemData.sovereignty, 'corporation'); + + if(sovDataFact){ + sovereigntyPrimary = { + row1Val: 'Faction', + row1Img: Util.eveImageUrl('alliance', sovDataFact.id, 64), + row1ImgTitle: sovDataFact.name, + row2Val: sovDataFact.name + }; + }else{ + if(sovDataAlly){ + sovereigntyPrimary = { + row1Val: 'Alliance', + row1Img: Util.eveImageUrl('alliance', sovDataAlly.id, 64), + row1ImgTitle: sovDataAlly.name, + row2Val: '<' + sovDataAlly.ticker + '>', + row3Label: 'Ally', + row3Val: sovDataAlly.name + }; + } + if(sovDataCorp){ + sovereigntySecondary = { + row1Label: 'Corp', + row1Val: sovDataCorp.name, + row1Img: Util.eveImageUrl('corporation', sovDataCorp.id, 64) + }; + } + } + } + // system "static" wh data let staticsData = []; if( @@ -152,9 +322,15 @@ define([ let data = { system: systemData, + sovereigntyPrimary: sovereigntyPrimary ? Object.assign({}, sovereigntyDefault, sovereigntyPrimary) : undefined, + sovereigntySecondary: sovereigntySecondary ? Object.assign({}, sovereigntyDefault, sovereigntySecondary) : undefined, static: staticsData, moduleHeadlineIconClass: config.moduleHeadlineIconClass, - tableClass: config.systemInfoTableClass, + infoSectionClass: config.systemInfoSectionClass, + descriptionSectionClass: config.descriptionSectionClass, + sovSectionClass: config.systemSovSectionClass, + infoTableClass: config.systemInfoTableClass, + sovTableClass: config.systemSovTableClass, nameInfoClass: config.systemInfoNameClass, effectInfoClass: config.systemInfoEffectClass, planetsInfoClass: config.systemInfoPlanetsClass, @@ -162,13 +338,21 @@ define([ statusInfoClass: config.systemInfoStatusLabelClass, popoverTriggerClass: Util.config.popoverTriggerClass, + // sovereignty table + sovFwContestedRowClass: config.systemSovFwContestedRowClass, + sovFwOccupationRowClass: config.systemSovFwOccupationRowClass, + sovFwContestedInfoClass: config.systemSovFwContestedClass, + sovFwPercentageInfoClass: config.systemSovFwPercentageClass, + sovFwOccupationInfoClass: config.systemSovFwOccupationClass, + sovFwOccupationImageClass: config.systemSovFwOccupationImageClass, + sovFwStatusIconClass: config.systemSovFwStatusIconClass, + systemUrl: MapUtil.getMapDeeplinkUrl(mapId, systemData.id), systemTypeName: MapUtil.getSystemTypeInfo(systemData.type.id, 'name'), - systemIsWormhole: MapUtil.getSystemTypeInfo(systemData.type.id, 'name') === 'w-space', systemStatusId: systemData.status.id, systemStatusClass: Util.getStatusInfoForSystem(systemData.status.id, 'class'), systemStatusLabel: Util.getStatusInfoForSystem(systemData.status.id, 'label'), - securityClass: Util.getSecurityClassForSystem( systemData.security ), + securityClass: Util.getSecurityClassForSystem(systemData.security), trueSec: systemData.trueSec.toFixed(1), trueSecClass: Util.getTrueSecClassForSystem( systemData.trueSec ), effectName: effectName, @@ -194,19 +378,23 @@ define([ systemConstellationLinkClass: config.constellationLinkClass, systemRegionLinkClass: config.regionLinkClass, systemTypeLinkClass: config.typeLinkClass, - systemUrlLinkClass: config.urlLinkClass + systemUrlLinkClass: config.urlLinkClass, + ccpImageServerUrl: Init.url.ccpImageServer, + thirdPartyLinks: getThirdPartySystemLinks(['eve', 'dotlan', 'eveeye', 'anoik'], systemData) }; requirejs(['text!templates/modules/system_info.html', 'mustache', 'summernote.loader'], (template, Mustache, Summernote) => { let content = Mustache.render(template, data); moduleElement.append(content); + let sovSectionArea = moduleElement.find('.' + config.systemSovSectionClass + ' .' + Util.config.dynamicAreaClass); let descriptionArea = moduleElement.find('.' + config.descriptionAreaClass); let descriptionButton = moduleElement.find('.' + config.addDescriptionButtonClass); let descriptionTextareaElement = moduleElement.find('.' + config.descriptionTextareaElementClass); // lock "description" field until first update descriptionArea.showLoadingAnimation(); + sovSectionArea.showLoadingAnimation(); // WYSIWYG init on button click --------------------------------------------------------------------------- descriptionButton.on('click', function(e){ diff --git a/js/app/ui/module/system_intel.js b/js/app/ui/module/system_intel.js index 753e2b19..6b3b9459 100644 --- a/js/app/ui/module/system_intel.js +++ b/js/app/ui/module/system_intel.js @@ -7,8 +7,9 @@ define([ 'app/init', 'app/util', 'bootbox', - 'app/counter' -], ($, Init, Util, bootbox, Counter) => { + 'app/counter', + 'app/map/util', +], ($, Init, Util, bootbox, Counter, MapUtil) => { 'use strict'; let config = { @@ -18,7 +19,7 @@ define([ moduleHeadClass: 'pf-module-head', // class for module header moduleHandlerClass: 'pf-module-handler-drag', // class for "drag" handler - // system info module + // system intel module moduleTypeClass: 'pf-system-intel-module', // class for this module // headline toolbar @@ -28,10 +29,14 @@ define([ moduleHeadlineIconRefreshClass: 'pf-module-icon-button-refresh', // class for "refresh" icon // system intel module - systemStructuresTableClass: 'pf-system-structure-table', // class for route tables + intelTableId: 'pf-intel-table-', // id prefix for all tables in module + intelTableRowIdPrefix: 'pf-intel-row-', // id prefix for table rows + systemStationsTableClass: 'pf-system-station-table', // class for NPC owned stations table + systemStructuresTableClass: 'pf-system-structure-table', // class for player owned structures table // structure dialog structureDialogId: 'pf-structure-dialog', // id for "structure" dialog + nameInputId: 'pf-structure-dialog-name-input', // id for "name" input statusSelectId: 'pf-structure-dialog-status-select', // id for "status" select typeSelectId: 'pf-structure-dialog-type-select', // id for "type" select corporationSelectId: 'pf-structure-dialog-corporation-select', // id for "corporation" select @@ -39,11 +44,12 @@ define([ descriptionTextareaCharCounter: 'pf-form-field-char-count', // class for "character counter" element for form field // dataTable - tableRowIdPrefix: 'pf-structure-row_', // id prefix for table rows tableCellImageClass: 'pf-table-image-smaller-cell', // class for table "image" cells tableCellCounterClass: 'pf-table-counter-cell', // class for table "counter" cells tableCellEllipsisClass: 'pf-table-cell-ellipses-auto', // class for table "ellipsis" cells - dataTableActionCellClass: 'pf-table-action-cell' // class for "action" cells + tableCellActionClass: 'pf-table-action-cell', // class for "action" cells + tableCellActionIconClass: 'pf-table-action-icon-cell', // class for table "action" icon (icon is part of cell content) + tableCellServicesClass: 'pf-table-services-cell' // class for table station "services" cells }; let maxDescriptionLength = 512; @@ -53,26 +59,102 @@ define([ * @param statusData * @returns {string} */ - let getStatusData = statusData => { + let getIconForStatusData = statusData => { return ''; }; + /** + * get icon that marks a table cell as clickable + * @returns {string} + */ + let getIconForInformationWindow = () => { + return ''; + }; + + /** + * get dataTable id + * @param mapId + * @param systemId + * @param tableType + * @returns {string} + */ + let getTableId = (tableType, mapId, systemId) => Util.getTableId(config.intelTableId, tableType, mapId, systemId); + + /** + * get dataTable row id + * @param tableType + * @param id + * @returns {string} + */ + let getRowId = (tableType, id) => Util.getTableRowId(config.intelTableRowIdPrefix, tableType, id); + /** * get
' + + '' + + '' + group.name + '
', { + // "Structures" table ----------------------------------------------------------------------------------------- + let structureTable = $('
', { + id: getTableId('structure', mapId, systemData.id), class: ['compact', 'stripe', 'order-column', 'row-border', 'pf-table-fixed', config.systemStructuresTableClass].join(' ') }); - moduleElement.append(table); + moduleElement.append(structureTable); - let tableApi = table.DataTable({ - paging: false, - lengthChange: false, - ordering: true, - order: [[ 10, 'desc' ], [ 0, 'asc' ]], - info: false, - searching: false, - hover: false, - autoWidth: false, - rowId: rowData => config.tableRowIdPrefix + rowData.id, + let structureDataTableOptions = { + pfMeta: { + type: 'structures' + }, + order: [[10, 'desc' ], [0, 'asc' ]], + rowId: rowData => getRowId('structures', rowData.id), language: { emptyTable: 'No structures recorded', info: '_START_ to _END_ of _MAX_', infoEmpty: '' }, - rowGroup: { - enable: true, - dataSrc: 'systemId' - }, columnDefs: [ { targets: 0, @@ -433,17 +622,17 @@ define([ className: ['text-center', 'all'].join(' '), data: 'status', render: { - display: data => getStatusData(data), + display: data => getIconForStatusData(data), sort: data => data.id }, createdCell: function(cell, cellData, rowData, rowIndex, colIndex){ - $(cell).find('i').tooltip(); + $(cell).find('i').tooltip(); } },{ targets: 1, name: 'structureImage', title: '', - width: 26, + width: 24, orderable: false, className: [config.tableCellImageClass, 'text-center', 'all'].join(' '), data: 'structure.id', @@ -452,7 +641,7 @@ define([ _: function(data, type, row, meta){ let value = data; if(type === 'display' && value){ - value = ''; + value = ''; } return value; } @@ -476,7 +665,7 @@ define([ targets: 4, name: 'ownerImage', title: '', - width: 26, + width: 24, orderable: false, className: [config.tableCellImageClass, 'text-center', 'all'].join(' '), data: 'owner.id', @@ -486,7 +675,7 @@ define([ let value = data; if(type === 'display' && value){ value = ''; - value += ''; + value += ''; value += ''; } return value; @@ -519,12 +708,12 @@ define([ title: '', orderable: false, width: 10, - className: ['text-center', config.dataTableActionCellClass, config.moduleHeadlineIconClass, 'all'].join(' '), + className: ['text-center', config.tableCellActionClass, config.moduleHeadlineIconClass, 'all'].join(' '), data: null, render: { display: data => { let icon = ''; - if(data.corporation.id !== corporationId){ + if(data.rowGroupData.id !== corporationId){ icon = ''; } return icon; @@ -534,7 +723,7 @@ define([ let tableApi = this.api(); if($(cell).is(':empty')){ - $(cell).removeClass(config.dataTableActionCellClass + ' ' + config.moduleHeadlineIconClass); + $(cell).removeClass(config.tableCellActionClass + ' ' + config.moduleHeadlineIconClass); }else{ $(cell).on('click', function(e){ // get current row data (important!) @@ -550,12 +739,12 @@ define([ title: '', orderable: false, width: 10, - className: ['text-center', config.dataTableActionCellClass, 'all'].join(' '), + className: ['text-center', config.tableCellActionClass, 'all'].join(' '), data: null, render: { display: data => { let icon = ''; - if(data.corporation.id !== corporationId){ + if(data.rowGroupData.id !== corporationId){ icon = ''; } return icon; @@ -565,19 +754,15 @@ define([ let tableApi = this.api(); if($(cell).find('.fa-ban').length){ - $(cell).removeClass(config.dataTableActionCellClass + ' ' + config.moduleHeadlineIconClass); + $(cell).removeClass(config.tableCellActionClass + ' ' + config.moduleHeadlineIconClass); $(cell).find('i').tooltip(); }else{ let confirmationSettings = { - container: 'body', - placement: 'left', - btnCancelClass: 'btn btn-sm btn-default', - btnCancelLabel: 'cancel', - btnCancelIcon: 'fas fa-fw fa-ban', title: 'delete structure', - btnOkClass: 'btn btn-sm btn-danger', - btnOkLabel: 'delete', - btnOkIcon: 'fas fa-fw fa-times', + template: Util.getConfirmationTemplate(null, { + size: 'small', + noTitle: true + }), onConfirm : function(e, target){ // get current row data (important!) // -> "rowData" param is not current state, values are "on createCell()" state @@ -606,9 +791,9 @@ define([ } },{ targets: 10, - name: 'corporation', + name: 'rowGroupData', className: 'never', // never show this column. see: https://datatables.net/extensions/responsive/classes - data: 'corporation', + data: 'rowGroupData', visible: false, render: { sort: function(data){ @@ -617,40 +802,6 @@ define([ } } ], - drawCallback: function(settings){ - let tableApi = this.api(); - let columnCount = tableApi.columns(':visible').count(); - let rows = tableApi.rows( {page:'current'} ).nodes(); - let last= null; - - tableApi.column('corporation:name', {page:'current'} ).data().each( function(group, i ){ - if( !last || last.id !== group.id ){ - $(rows).eq(i).before( - '' + - '' + - '' + - '' + - '' - ); - last = group; - } - }); - - let animationRows = rows.to$().filter(function(){ - return ( - $(this).data('animationStatus') || - $(this).data('animationTimer') - ); - }); - - for(let i = 0; i < animationRows.length; i++){ - let animationRow = $(animationRows[i]); - animationRow.pulseBackgroundColor(animationRow.data('animationStatus')); - animationRow.removeData('animationStatus'); - } - }, initComplete: function(settings){ // table data is load in updateModule() method // -> no need to trigger additional ajax call here for data @@ -659,15 +810,264 @@ define([ Counter.initTableCounter(this, ['updated:name'], 'd'); } - }); + }; - new $.fn.dataTable.Responsive(tableApi); + $.extend(true, structureDataTableOptions, getDataTableDefaults()); + let tableApiStructure = structureTable.DataTable(structureDataTableOptions); - tableApi.on('responsive-resize', function(e, tableApi, columns){ + new $.fn.dataTable.Responsive(tableApiStructure); + + tableApiStructure.on('responsive-resize', function(e, tableApi, columns){ // rowGroup length changes as well -> trigger draw() updates rowGroup length (see drawCallback()) tableApi.draw(); }); + if(showStationTable){ + // "Stations" table --------------------------------------------------------------------------------------- + + moduleElement.append( + $('
', { + class: config.moduleHeadClass + }).append( + $('
', { + class: config.moduleHandlerClass + }), + $('
', { + text: 'Stations' + }) + ) + ); + + let stationTable = $('
' + - '' + - '' + group.name + '
', { + id: getTableId('station', mapId, systemData.id), + class: ['compact', 'stripe', 'order-column', 'row-border', 'pf-table-fixed', config.systemStationsTableClass].join(' ') + }); + moduleElement.append(stationTable); + + let stationDataTableOptions = { + pfMeta: { + type: 'stations' + }, + order: [[1, 'asc' ], [8, 'asc' ]], + rowId: rowData => getRowId('stations', rowData.id), + language: { + emptyTable: 'No stations found', + info: '_START_ to _END_ of _MAX_', + infoEmpty: '' + }, + columnDefs: [ + { + targets: 0, + name: 'stationImage', + title: '', + width: 24, + orderable: false, + className: [config.tableCellImageClass, 'text-center', 'all'].join(' '), + data: 'type.id', + defaultContent: '', + render: { + _: function(data, type, row, meta){ + let value = data; + if(type === 'display' && value){ + value = ''; + } + return value; + } + } + },{ + targets: 1, + name: 'count', + title: '', + width: 5, + className: ['text-center', 'all'].join(' '), + data: 'name', + render: { + _: function(cellData, type, rowData, meta){ + let value = ''; + if(cellData){ + // "grouped" regex not supported by FF + // let matches = /^(?[a-z0-9\s\-]+) (?[MDCLXVI]+) .*$/i.exec(cellData); + // let count = Util.getObjVal(matches, 'groups.count'); + let matches = /^([a-z0-9\s\-]+) ([MDCLXVI]+) .*$/i.exec(cellData); + let count = Util.getObjVal(matches, '2'); + + if(type === 'display'){ + value = count || 0; + }else{ + value = romanToInt(count) || ''; + } + } + + return value; + } + } + },{ + targets: 2, + name: 'name', + title: 'station', + className: [config.tableCellEllipsisClass, 'all'].join(' '), + data: 'name', + render: { + _: function(cellData, type, rowData, meta){ + let value = cellData; + if(cellData){ + // "grouped" regex not supported by FF + // let matches = /^(?[a-z0-9\s\-]+) (?[MDCLXVI]+) (?
', { + id: getTableId('info', mapId, systemData.id), + class: ['display', 'compact', 'nowrap', config.sigTableClass, config.sigTableInfoClass].join(' ') + }); + + infoElement.append(table); + + let dataTableOptions = { + tabIndex: -1, + dom: '<"row"<"col-xs-3"l><"col-xs-5 ' + config.tableToolbarStatusClass + '"><"col-xs-4"f>>' + + '<"row"<"col-xs-12"tr>>' + + '<"row"<"col-xs-5"i><"col-xs-7"p>>', + initComplete: function(settings, json){ + let tableApi = this.api(); + + initCharacterInfoTooltip(this, tableApi); + + tableApi.columns(['action:name']).visible(false); + + Counter.initTableCounter(this, ['created:name', 'updated:name']); + } + }; + + $.extend(true, dataTableOptions, getSignatureDataTableDefaults(mapId, systemData)); + + let tableApi = table.DataTable(dataTableOptions); + + tableApi.on('draw.dt', function(e, settings){ + // xEditable cells should not be editable in this table + $(dialogElement).find('.' + config.sigTableInfoClass).find('td.editable').editable('disable'); + }); + + return tableApi; + }; + /** * draw signature table toolbar (add signature button, scan progress bar * @param moduleElement @@ -1880,7 +2235,7 @@ define([ // create "empty table for new signature let table = $('
', { - id: getTableId(mapId, systemData.id, 'secondary'), + id: getTableId('secondary', mapId, systemData.id), class: ['stripe', 'row-border', 'compact', 'nowrap', config.sigTableClass, config.sigTableSecondaryClass].join(' ') }); @@ -2020,8 +2375,6 @@ define([ editable.input.$input.first().prop('disabled', true); // preselect second option //editable.input.$input.eq(1).prop('checked', true); - //editable.setValue('ad78172b72d0327b237c4a7dc1daa5d7'); - // "fake" radio button behaviour editable.input.$input.attr('name', 'test').attr('type', 'radio'); @@ -2162,6 +2515,30 @@ define([ }); }; + /** + * init character info tooltips + * -> e.g. table cell 'question mark' icon + * @param element + * @param tableApi + */ + let initCharacterInfoTooltip = (element, tableApi) => { + element.hoverIntent({ + over: function(e){ + let cellElement = $(this); + let rowData = tableApi.row(cellElement.parents('tr')).data(); + cellElement.addCharacterInfoTooltip(rowData, { + trigger: 'manual', + placement: 'top', + show: true + }); + }, + out: function(e){ + $(this).destroyPopover(); + }, + selector: 'td.' + Util.config.helpClass + }); + }; + /** * draw empty signature table * @param moduleElement @@ -2170,7 +2547,7 @@ define([ */ let drawSignatureTable = (moduleElement, mapId, systemData) => { let table = $('
', { - id: getTableId(mapId, systemData.id, 'primary'), + id: getTableId('primary', mapId, systemData.id), class: ['display', 'compact', 'nowrap', config.sigTableClass, config.sigTablePrimaryClass].join(' ') }); @@ -2213,6 +2590,7 @@ define([ initComplete: function(settings, json){ let tableApi = this.api(); + initCharacterInfoTooltip(this, tableApi); initGroupFilterButton(tableApi); initUndoButton(tableApi); initSelectAllButton(tableApi); @@ -2386,13 +2764,13 @@ 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: lazyUpdateToggle.hasClass('active') ? 1 : 0 + deleteOld: getLazyUpdateToggleStatus(moduleElement), + deleteConnection: 0 }; // "disable" lazy update icon -> prevents accidental removal for next paste #724 - lazyUpdateToggle.toggleClass('active', false); + getLazyUpdateToggleElement(moduleElement).toggleClass('active', false); updateSignatureTableByClipboard(e.data.tableApi, systemData, clipboard, signatureOptions); }); @@ -2402,23 +2780,38 @@ define([ moduleElement.find('.' + config.sigTableClass), '.editable-click:not(.editable-open) span[class^="pf-system-sec-"]' ); + }; - // signature column - "info" popover -------------------------------------------------------------------------- - moduleElement.find('.' + config.sigTablePrimaryClass).hoverIntent({ - over: function(e){ - let cellElement = $(this); - let rowData = primaryTableApi.row(cellElement.parents('tr')).data(); - cellElement.addCharacterInfoTooltip(rowData, { - trigger: 'manual', - placement: 'top', - show: true - }); - }, - out: function(e){ - $(this).destroyPopover(); - }, - selector: 'td.' + Util.config.helpClass - }); + /** + * get "lazy delete" toggle element + * @param moduleElement + * @returns {*} + */ + let getLazyUpdateToggleElement = moduleElement => moduleElement.find('.' + config.moduleHeadlineIconLazyClass); + + /** + * get status for "lazy delete" toggle + * @param moduleElement + * @returns {number} + */ + let getLazyUpdateToggleStatus = moduleElement => getLazyUpdateToggleElement(moduleElement).hasClass('active') ? 1 : 0; + + /** + * update 'counter' UI elements in 'signature reader' dialog + * @param data + */ + let updateSignatureReaderCounters = data => { + let counterElement = $('#' + config.sigInfoCountSigNewId).text(data.added || 0); + counterElement.toggleClass(counterElement.attr('data-class'), Boolean(data.added)); + + counterElement = $('#' + config.sigInfoCountSigChangeId).text(data.changed || 0); + counterElement.toggleClass(counterElement.attr('data-class'), Boolean(data.changed)); + + counterElement = $('#' + config.sigInfoCountSigDeleteId).text(data.deleted || 0); + counterElement.toggleClass(counterElement.attr('data-class'), Boolean(data.deleted)); + + counterElement = $('#' + config.sigInfoCountConDeleteId).text(data.deleteCon || 0); + counterElement.toggleClass(counterElement.attr('data-class'), Boolean(data.deleteCon)); }; /** @@ -2435,6 +2828,181 @@ define([ return row; }; + /** + * @param action + * @param rowId + * @returns {Promise} + */ + let getPromiseForRow = (action, rowId) => { + return new Promise((resolve, reject) => { + resolve({action: action, rowId: rowId}); + }); + }; + + /** + * callback for a changed row + * @param rowIndex + * @param colIndex + * @param tableLoopCount + * @param cellLoopCount + * @param options CUSTOM parameter (not DataTables specific)! + */ + let rowUpdate = function(rowIndex, colIndex, tableLoopCount, cellLoopCount, options){ + let cell = this; + let node = cell.nodes().to$(); + if(node.data('editable')){ + // xEditable is active -> should always be active! + // set new value even if no change -> e.g. render selected Ids as text labels + let oldValue = node.editable('getValue', true); + + // ... some editable cells depend on each other (e.g. group->type, group->connection) + switch(node.data('editable').options.name){ + case 'typeId': + // ... disable if no type options found + editableSelectCheck(node); + break; + case 'connectionId': + // disables if no wormhole group set + let groupId = cell.cell(rowIndex, 'group:name').data(); + if(groupId === 5){ + // wormhole + editableEnable(node); + }else{ + editableDisable(node); + } + break; + } + + // values should be set AFTER en/disabling of a field + node.editable('setValue', cell.data()); + + if(oldValue !== cell.data()){ + // highlight cell on data change + node.pulseBackgroundColor('changed', Util.getObjVal(options, 'keepVisible') || false); + } + }else if(node.hasClass(config.tableCellCounterClass)){ + // "updated" timestamp always changed + node.pulseBackgroundColor('changed', Util.getObjVal(options, 'keepVisible') || false); + } + }; + + /** + * update 'info' (preview) signature table (inside 'signature reader' dialog) + * @param tableApi + * @param signaturesDataOrig + * @param deleteOutdatedSignatures + * @param deleteConnections + */ + let updateSignatureInfoTable = (tableApi, signaturesDataOrig, deleteOutdatedSignatures = false, deleteConnections = false) => { + // clone signature array because of further manipulation + let signaturesData = $.extend([], signaturesDataOrig); + + let rowIdsExist = []; + + let promisesAdded = []; + let promisesChanged = []; + let promisesDeleted = []; + + let allRows = tableApi.rows(); + + let rowUpdateCallback = function(){ + rowUpdate.apply(this, [...arguments, {keepVisible: true}]); + }; + + // update rows ------------------------------------------------------------------------------------------------ + allRows.every(function(rowIdx, tableLoop, rowLoop){ + let row = this; + let rowData = row.data(); + + for(let i = 0; i < signaturesData.length; i++){ + if(signaturesData[i].name === rowData.name){ + let rowId = row.id(true); + + // check if row was updated + if(signaturesData[i].updated.updated > rowData.updated.updated){ + // set new row data -> draw() is executed after all changes made + let newRowData = signaturesData[i]; + // keep "description" must not be replaced + newRowData.description = rowData.description; + // existing "groupId" must not be removed + if(!newRowData.groupId){ + newRowData.groupId = rowData.groupId; + newRowData.typeId = rowData.typeId; + }else if(newRowData.groupId === rowData.groupId){ + if(!newRowData.typeId){ + newRowData.typeId = rowData.typeId; + } + } + + // "created" timestamp will not change -> use existing + newRowData.created = rowData.created; + row.data(newRowData); + + // bind new signature dataTable data() -> to xEditable inputs + row.cells(row.id(true), ['id:name', 'group:name', 'type:name', 'description:name', 'connection:name', 'updated:name']) + .every(rowUpdateCallback); + + promisesChanged.push(getPromiseForRow('changed', rowId)); + } + + rowIdsExist.push(rowIdx); + + // remove signature data -> all left signatures will be added + signaturesData.splice(i, 1); + i--; + } + } + }); + + // delete rows ------------------------------------------------------------------------------------------------ + if(deleteOutdatedSignatures){ + let rows = tableApi.rows((rowIdx, rowData, node) => !rowIdsExist.includes(rowIdx)); + rows.every(function(rowIdx, tableLoop, rowLoop){ + let row = this; + let rowId = row.id(true); + let rowElement = row.nodes().to$(); + let rowData = row.data(); + + rowElement.pulseBackgroundColor('deleted', true); + + promisesChanged.push(getPromiseForRow('deleted', rowId)); + + // check if there is a connectionId. + if(deleteConnections && Util.getObjVal(rowData, 'connection.id')){ + promisesChanged.push(getPromiseForRow('deleteCon', rowId)); + } + }); + } + + // add rows --------------------------------------------------------------------------------------------------- + for(let signatureData of signaturesData){ + let row = addSignatureRow(tableApi, signatureData); + let rowElement = row.nodes().to$(); + rowElement.pulseBackgroundColor('added', true); + + promisesAdded.push(getPromiseForRow('added', row.index())); + } + + // done ------------------------------------------------------------------------------------------------------- + Promise.all(promisesAdded.concat(promisesChanged, promisesDeleted)).then(payloads => { + if(payloads.length){ + // table data changed -> draw() table changes + tableApi.draw(); + + // no notifications if table was empty just progressbar notification is needed + // sum payloads by "action" + let notificationCounter = payloads.reduce((acc, payload) => { + acc[payload.action]++; + return acc; + }, Object.assign({}, emptySignatureReaderCounterData)); + + updateSignatureReaderCounters(notificationCounter); + + updateScannedSignaturesBar(tableApi, {showNotice: false}); + } + }); + }; + /** * update signature table with new signatures * -> add/update/delete rows @@ -2463,51 +3031,6 @@ define([ let allRows = tableApi.rows(); let updateEmptyTable = !allRows.any(); - let rowUpdate = function(rowIndex, colIndex, tableLoopCount, cellLoopCount){ - let cell = this; - let node = cell.nodes().to$(); - if(node.data('editable')){ - // xEditable is active -> should always be active! - // set new value even if no change -> e.g. render selected Ids as text labels - let oldValue = node.editable('getValue', true); - - // ... some editable cells depend on each other (e.g. group->type, group->connection) - switch(node.data('editable').options.name){ - case 'typeId': - // ... disable if no type options found - editableSelectCheck(node); - break; - case 'connectionId': - // disables if no wormhole group set - let groupId = cell.cell(rowIndex, 'group:name').data(); - if(groupId === 5){ - // wormhole - editableEnable(node); - }else{ - editableDisable(node); - } - break; - } - - // values should be set AFTER en/disabling of a field - node.editable('setValue', cell.data()); - - if(oldValue !== cell.data()){ - // highlight cell on data change - node.pulseBackgroundColor('changed'); - } - }else if(node.hasClass(config.tableCellCounterClass)){ - // "updated" timestamp always changed - node.pulseBackgroundColor('changed'); - } - }; - - let getPromiseForRow = (action, rowId) => { - return new Promise((resolve, reject) => { - resolve({action: action, rowId: rowId}); - }); - }; - // update signatures ------------------------------------------------------------------------------------------ allRows.every(function(rowIdx, tableLoop, rowLoop){ let row = this; @@ -2588,7 +3111,7 @@ define([ } acc[payload.action]++; return acc; - }, {}); + }, Object.assign({}, emptySignatureReaderCounterData)); let notification = ''; if(notificationCounter.added > 0){ @@ -2606,6 +3129,13 @@ define([ } updateScannedSignaturesBar(tableApi, {showNotice: true}); + + // at this point the 'primary' signature table update is done + // we need to check if there is an open 'signature reader' dialog, + // that needs to update its 'preview' signature table + // -> to use DataTables "drawCallback" option or "draw.dt" event is not the *best* option: + // Both are called to frequently (e.g. after filter/sort actions) + $('.' + config.sigReaderDialogClass + '.in').trigger('pf:updateSignatureReaderDialog'); } // unlock table diff --git a/js/app/util.js b/js/app/util.js index c1547800..7986f75d 100644 --- a/js/app/util.js +++ b/js/app/util.js @@ -75,14 +75,14 @@ define([ select2ImageLazyLoadClass: 'pf-select2-image-lazyLoad', // animation - animationPulseSuccessClass: 'pf-animation-pulse-success', // animation class - animationPulseWarningClass: 'pf-animation-pulse-warning', // animation class - animationPulseDangerClass: 'pf-animation-pulse-danger', // animation class + animationPulseClassPrefix: 'pf-animation-pulse-', // class prefix for "pulse" background animation // popover + popoverClass: 'pf-popover', // class for "popover" - custom modifier popoverTriggerClass: 'pf-popover-trigger', // class for "popover" trigger elements - popoverSmallClass: 'pf-popover-small', // class for small "popover" + popoverSmallClass: 'popover-small', // class for small "popover" popoverCharacterClass: 'pf-popover-character', // class for character "popover" + popoverListIconClass: 'pf-popover-list-icon', // class for list "icon"s in " // Summernote summernoteClass: 'pf-summernote', // class for Summernote "WYSIWYG" elements @@ -398,40 +398,6 @@ define([ return valid; }; - /** - * check multiple element if they are currently visible in viewport - * @returns {Array} - */ - $.fn.isInViewport = function(){ - let visibleElement = []; - - this.each(function(){ - let element = $(this)[0]; - - let top = element.offsetTop; - let left = element.offsetLeft; - let width = element.offsetWidth; - let height = element.offsetHeight; - - while(element.offsetParent){ - element = element.offsetParent; - top += element.offsetTop; - left += element.offsetLeft; - } - - if( - top < (window.pageYOffset + window.innerHeight) && - left < (window.pageXOffset + window.innerWidth) && - (top + height) > window.pageYOffset && - (left + width) > window.pageXOffset - ){ - visibleElement.push(this); - } - }); - - return visibleElement; - }; - /** * init the map-update-counter as "easy-pie-chart" */ @@ -588,7 +554,7 @@ define([ userData: userData, otherCharacters: () => { return userData.characters.filter((character, i) => { - let characterImage = Init.url.ccpImageServer + '/Character/' + character.id + '_32.jpg'; + let characterImage = eveImageUrl('character', character.id); // preload image (prevent UI flicker let img= new Image(); img.src = characterImage; @@ -642,7 +608,7 @@ define([ container: 'body', content: content, animation: false - }).data('bs.popover').tip().addClass('pf-popover'); + }).data('bs.popover').tip().addClass(config.popoverClass); button.popover('show'); @@ -799,28 +765,30 @@ define([ /** * highlight jquery elements * add/remove css class for keyframe animation - * @returns {any|JQuery|*} + * @param status + * @param keepVisible + * @param clear + * @returns {void|*|undefined} */ - $.fn.pulseBackgroundColor = function(status, clear){ - - let animationClass = ''; + $.fn.pulseBackgroundColor = function(status, keepVisible = false, clear = false){ + let animationClass = config.animationPulseClassPrefix; switch(status){ - case 'added': - animationClass = config.animationPulseSuccessClass; - break; - case 'changed': - animationClass = config.animationPulseWarningClass; - break; - case 'deleted': - animationClass = config.animationPulseDangerClass; - break; + case 'added': animationClass += 'success'; break; + case 'changed': animationClass += 'warning'; break; + case 'deleted': animationClass += 'danger'; break; + default: console.warn('Invalid status: %s', status); + } + + // if keepVisible -> background color animation class will not be deleted + if(keepVisible){ + animationClass += '-keep'; } let clearTimer = element => { - element.removeClass( animationClass ); + element.removeClass(animationClass); let currentTimer = element.data('animationTimer'); - if( animationTimerCache.hasOwnProperty(currentTimer) ){ + if(animationTimerCache.hasOwnProperty(currentTimer)){ clearTimeout( currentTimer ); delete animationTimerCache[currentTimer]; element.removeData('animationTimer'); @@ -830,18 +798,20 @@ define([ return this.each(function(){ let element = $(this); - if( element.hasClass(animationClass) ){ + if(element.hasClass(animationClass)){ // clear timer -> set new timer clearTimer(element); } - if(clear !== true){ + if(!clear){ element.addClass(animationClass); - let timer = setTimeout(clearTimer, 1500, element); - element.data('animationTimer', timer); - animationTimerCache[timer] = true; + // remove class after animation finish, if not 'keepVisible' + if(!keepVisible){ + let timer = setTimeout(clearTimer, 1500, element); + element.data('animationTimer', timer); + animationTimerCache[timer] = true; + } } - }); }; @@ -884,6 +854,23 @@ define([ */ let showVersionInfo = () => Con.showVersionInfo(getVersion()); + /** + * get CCP image URLs for + * @param type 'alliance'|'corporation'|'character'|'type'|'render' + * @param id + * @param size + * @returns {boolean} + */ + let eveImageUrl = (type, id, size = 32) => { + let url = false; + if(typeof type === 'string' && typeof id === 'number' && typeof size === 'number'){ + type = type.capitalize(); + let format = type === 'Character' ? 'jpg' : 'png'; + url = Init.url.ccpImageServer + '/' + type + '/' + id + '_' + size + '.' + format; + } + return url; + }; + /** * polyfill for "passive" events * -> see https://github.com/zzarcon/default-passive-events @@ -1005,6 +992,14 @@ define([ }); }; + /** + * capitalize first letter + * @returns {string} + */ + String.prototype.capitalize = function(){ + return this.charAt(0).toUpperCase() + this.slice(1); + }; + /** * get hash from string * @returns {number} @@ -1020,21 +1015,238 @@ define([ return hash; }; + String.prototype.trimLeftChars = function(charList){ + if(charList === undefined) + charList = '\\s'; + return this.replace(new RegExp('^[' + charList + ']+'), ''); + }; + + String.prototype.trimRightChars = function(charList){ + if(charList === undefined) + charList = '\\s'; + return this.replace(new RegExp('[' + charList + ']+$'), ''); + }; + + String.prototype.trimChars = function(charList){ + return this.trimLeftChars(charList).trimRightChars(charList); + }; + initPassiveEvents(); }; /** - * - * @param element + * filter elements from elements array that are not within viewport + * @param elements + * @returns {[]} */ - let initPageScroll = (element) => { - $(element).on('click', '.page-scroll', function(){ - // scroll to ancor element - $($(this).attr('data-anchor')).velocity('scroll', { - duration: 300, - easing: 'swing' - }); - }); + let findInViewport = elements => { + let visibleElement = []; + + for(let element of elements){ + if(!(element instanceof HTMLElement)){ + console.warn('findInViewport() expects Array() of %O; %o given', HTMLElement, element); + continue; + } + + let top = element.offsetTop; + let left = element.offsetLeft; + let width = element.offsetWidth; + let height = element.offsetHeight; + let origElement = element; + + while(element.offsetParent){ + element = element.offsetParent; + top += element.offsetTop; + left += element.offsetLeft; + } + + if( + top < (window.pageYOffset + window.innerHeight) && + left < (window.pageXOffset + window.innerWidth) && + (top + height) > window.pageYOffset && + (left + width) > window.pageXOffset + ){ + visibleElement.push(origElement); + } + } + + return visibleElement; + }; + + /** + * "Scroll Spy" implementation + * @see https://github.com/cferdinandi/gumshoe/blob/master/src/js/gumshoe/gumshoe.js + * @param navElement + * @param scrollElement + * @param settings + */ + let initScrollSpy = (navElement, scrollElement = window, settings = {}) => { + let timeout, current; + + let contents = Array.from(navElement.querySelectorAll('.page-scroll')).map(link => ({ + link: link, + content: document.getElementById(link.getAttribute('data-target')) + })); + + let getOffset = settings => { + if(typeof settings.offset === 'function'){ + return parseFloat(settings.offset()); + } + // Otherwise, return it as-is + return parseFloat(settings.offset); + }; + + let getDocumentHeight = () => { + return Math.max( + document.body.scrollHeight, document.documentElement.scrollHeight, + document.body.offsetHeight, document.documentElement.offsetHeight, + document.body.clientHeight, document.documentElement.clientHeight + ); + }; + + let activate = item => { + if(!item) return; + + // Get the parent list item + let li = item.link.closest('li'); + if(!li) return; + + // Add the active class to li + li.classList.add('active'); + }; + + let deactivate = item => { + if(!item) return; + + // remove focus + if(document.activeElement === item.link){ + document.activeElement.blur(); + } + + // Get the parent list item + let li = item.link.closest('li'); + if(!li) return; + + // Remove the active class from li + li.classList.remove('active'); + }; + + let isInView = (elem, settings, bottom) => { + let bounds = elem.getBoundingClientRect(); + let offset = getOffset(settings); + if(bottom){ + return parseInt(bounds.bottom, 10) < (window.innerHeight || document.documentElement.clientHeight); + } + return parseInt(bounds.top, 10) <= offset; + }; + + let isAtBottom = () => { + return window.innerHeight + window.pageYOffset >= getDocumentHeight(); + }; + + let useLastItem = (item, settings) => { + return !!(isAtBottom() && isInView(item.content, settings, true)); + }; + + let getActive = (contents, settings) => { + let last = contents[contents.length - 1]; + if(useLastItem(last, settings)) return last; + for(let i = contents.length - 1; i >= 0; i--){ + if(isInView(contents[i].content, settings)) return contents[i]; + } + }; + + let detect = () => { + let active = getActive(contents, settings); + + // if there's no active content, deactivate and bail + if(!active){ + if(current){ + deactivate(current); + current = null; + } + return; + } + + // If the active content is the one currently active, do nothing + if (current && active.content === current.content) return; + + // Deactivate the current content and activate the new content + deactivate(current); + activate(active); + + // Update the currently active content + current = active; + }; + + let scrollHandler = () => { + // If there's a timer, cancel it + if(timeout){ + window.cancelAnimationFrame(timeout); + } + timeout = window.requestAnimationFrame(detect); + }; + + // Find the currently active content + detect(); + + scrollElement.addEventListener('scroll', scrollHandler, false); + + // set click observer for links + let clickHandler = function(e){ + e.preventDefault(); + this.content.scrollIntoView({behavior: 'smooth'}); + }; + + for(let item of contents){ + $(item.link).on('click', clickHandler.bind(item)); + } + }; + + /** + * get template for Bootstrap "Confirmation" popover plugin + * -> if HTML 'content' not set, we expect the default template + * https://www.npmjs.com/package/bs-confirmation + * -> options.size for "small" popover layout + * -> options.noTitle for hide title element + * @param content + * @param options + * @returns {string} + */ + let getConfirmationTemplate = (content, options) => { + let getButtons = () => { + let buttonHtml = '
'; + buttonHtml += 'Yes'; + buttonHtml += 'No'; + buttonHtml += '
'; + return buttonHtml; + }; + + let getContent = content => { + let contentHtml = content ? content : ''; + contentHtml += ''; + return contentHtml; + }; + + let popoverClass = ['popover']; + if('small' === getObjVal(options, 'size')){ + popoverClass.push('popover-small'); + } + + let contentClass = ['popover-content', 'no-padding']; + + let html = '
'; + html += '
'; + if(true !== getObjVal(options, 'noTitle')){ + html += '

'; + } + html += '
'; + html += getContent(content); + html += '
'; + html += '
'; + return html; }; /** @@ -1118,7 +1330,7 @@ define([ }; /** - * set default configuration for "Bootbox" + * set default configuration for "Bootbox" plugin */ let initDefaultBootboxConfig = () => { bootbox.setDefaults({ @@ -1127,7 +1339,22 @@ define([ }; /** - * set default configuration for "Select2" + * set default configuration for "Confirmation" popover plugin + */ + let initDefaultConfirmationConfig = () => { + $.fn.confirmation.Constructor.DEFAULTS.placement = 'left'; + $.fn.confirmation.Constructor.DEFAULTS.container = 'body'; + $.fn.confirmation.Constructor.DEFAULTS.btnCancelClass = 'btn btn-sm btn-default'; + $.fn.confirmation.Constructor.DEFAULTS.btnCancelLabel = 'cancel'; + $.fn.confirmation.Constructor.DEFAULTS.btnCancelIcon = 'fas fa-fw fa-ban'; + $.fn.confirmation.Constructor.DEFAULTS.btnOkClass = 'btn btn-sm btn-danger'; + $.fn.confirmation.Constructor.DEFAULTS.btnOkLabel = 'delete'; + $.fn.confirmation.Constructor.DEFAULTS.btnOkIcon = 'fas fa-fw fa-times'; + $.fn.confirmation.Constructor.DEFAULTS.template = getConfirmationTemplate(); + }; + + /** + * set default configuration for "Select2" plugin */ let initDefaultSelect2Config = () => { $.fn.select2.defaults.set('theme', 'pathfinder'); @@ -1251,7 +1478,7 @@ define([ }; /** - * set default configuration for "xEditable" + * set default configuration for "xEditable" plugin */ let initDefaultEditableConfig = () => { // use fontAwesome buttons template @@ -1341,7 +1568,7 @@ define([ // Server is running with GMT/UTC (EVE Time) let localDate = new Date(); - let serverDate= new Date( + let serverDate = new Date( localDate.getUTCFullYear(), localDate.getUTCMonth(), localDate.getUTCDate(), @@ -1577,6 +1804,8 @@ define([ characterLogLocation: valueChanged('character.logLocation'), characterSystemId: valueChanged('character.log.system.id'), characterShipType: valueChanged('character.log.ship.typeId'), + characterStationId: valueChanged('character.log.station.id'), + characterStructureId: valueChanged('character.log.structure.id'), charactersIds: oldCharactersIds.toString() !== newCharactersIds.toString(), characterLogHistory: oldHistoryLogStamps.toString() !== newHistoryLogStamps.toString() }; @@ -1645,8 +1874,8 @@ define([ * Request data from Server * -> This function should be used (in future) for all Ajax and REST API calls * -> works as a "wrapper" for jQueries ajax() method - * @param action - * @param entity + * @param {String} action + * @param {String} entity * @param ids * @param data * @param context @@ -1658,7 +1887,7 @@ define([ let requestExecutor = (resolve, reject) => { let payload = { action: 'request', - name: action.toLowerCase() + entity.charAt(0).toUpperCase() + entity.slice(1) + name: action.toLowerCase() + entity.capitalize() }; // build request url -------------------------------------------------------------------------------------- @@ -1917,6 +2146,13 @@ define([ return mapModule; }; + /** + * + * @param ariaId + * @returns {number} + */ + let getSystemEffectMultiplierByAreaId = ariaId => SystemEffect.getMultiplierByAreaId(ariaId); + /** * get areaId by security string * areaId is required as a key for signature names @@ -1936,9 +2172,6 @@ define([ case '0.0': areaId = 32; break; - case 'SH': - areaId = 13; - break; default: // w-space for(let i = 1; i <= 18; i++){ @@ -2085,7 +2318,7 @@ define([ let typeClass = ''; let matches = regex.exec(typeName.toLowerCase()); if(matches && matches[1]){ - typeName = matches[1].charAt(0).toUpperCase() + matches[1].slice(1); + typeName = matches[1].capitalize(); typeClass = getPlanetInfo(matches[1]); } @@ -2111,26 +2344,28 @@ define([ * get a HTML table with universe region information * e.g. for popover * @param regionName - * @param faction + * @param sovereignty * @returns {string} */ - let getSystemRegionTable = (regionName, faction) => { + let getSystemRegionTable = (regionName, sovereignty) => { + let data = [{label: 'Region', value: regionName}]; + if(sovereignty){ + if(sovereignty.faction){ + data.push({label: 'Sov. Faction', value: sovereignty.faction.name}); + } + if(sovereignty.alliance){ + data.push({label: 'Sov. Ally', value: sovereignty.alliance.name}); + } + } + let table = '
'; - table += ''; - table += ''; - table += ''; - table += ''; - table += ''; - if(faction){ + for(let rowData of data){ + table += ''; table += ''; table += ''; table += ''; } @@ -2675,11 +2910,12 @@ define([ }; /** - * set new destination for a system - * @param systemData + * set new destination for a system/station/structure * @param type + * @param destType + * @param destData */ - let setDestination = (systemData, type) => { + let setDestination = (type, destType, destData) => { let description = ''; switch(type){ case 'set_destination': @@ -2699,22 +2935,20 @@ define([ data: { clearOtherWaypoints: (type === 'set_destination') ? 1 : 0, first: (type === 'add_last_waypoint') ? 0 : 1, - systemData: [{ - systemId: systemData.systemId, - name: systemData.name - }] + destData: [destData] }, context: { + destType: destType, description: description }, dataType: 'json' }).done(function(responseData){ if( - responseData.systemData && - responseData.systemData.length > 0 + responseData.destData && + responseData.destData.length > 0 ){ - for(let j = 0; j < responseData.systemData.length; j++){ - showNotify({title: this.description, text: 'System: ' + responseData.systemData[j].name, type: 'success'}); + for(let j = 0; j < responseData.destData.length; j++){ + showNotify({title: this.description, text: this.destType + ': ' + responseData.destData[j].name, type: 'success'}); } } @@ -2723,7 +2957,7 @@ define([ responseData.error.length > 0 ){ for(let i = 0; i < responseData.error.length; i++){ - showNotify({title: this.description + ' error', text: 'System: ' + responseData.error[i].message, type: 'error'}); + showNotify({title: this.description + ' error', text: this.destType + ': ' + responseData.error[i].message, type: 'error'}); } } @@ -3043,7 +3277,16 @@ define([ * @param tableType * @returns {string} */ - let getTableId = (prefix, mapId, systemId, tableType) => prefix + [mapId, systemId, tableType].join('-'); + let getTableId = (prefix, tableType, mapId, systemId) => prefix + [tableType, mapId, systemId].join('-'); + + /** + * get dataTable row id + * @param prefix + * @param tableType + * @param rowId + * @returns {string} + */ + let getTableRowId = (prefix, tableType, rowId) => prefix + [tableType, rowId].join('-'); /** * get a dataTableApi instance from global cache @@ -3055,7 +3298,7 @@ define([ */ let getDataTableInstance = (prefix, mapId, systemId, tableType) => { let instance = null; - let table = $.fn.dataTable.tables({ visible: false, api: true }).table('#' + getTableId(prefix, mapId, systemId, tableType)); + let table = $.fn.dataTable.tables({ visible: false, api: true }).table('#' + getTableId(prefix, tableType, mapId, systemId)); if(table.node()){ instance = table; } @@ -3233,8 +3476,10 @@ define([ config: config, getVersion: getVersion, showVersionInfo: showVersionInfo, + eveImageUrl: eveImageUrl, initPrototypes: initPrototypes, initDefaultBootboxConfig: initDefaultBootboxConfig, + initDefaultConfirmationConfig: initDefaultConfirmationConfig, initDefaultSelect2Config: initDefaultSelect2Config, initDefaultEditableConfig: initDefaultEditableConfig, getCurrentTriggerDelay: getCurrentTriggerDelay, @@ -3258,6 +3503,7 @@ define([ getLabelByRole: getLabelByRole, getMapElementFromOverlay: getMapElementFromOverlay, getMapModule: getMapModule, + getSystemEffectMultiplierByAreaId: getSystemEffectMultiplierByAreaId, getSystemEffectData: getSystemEffectData, getSystemEffectTable: getSystemEffectTable, getSystemPlanetsTable: getSystemPlanetsTable, @@ -3287,7 +3533,9 @@ define([ getCurrentLocationData: getCurrentLocationData, getCurrentUserInfo: getCurrentUserInfo, getCurrentCharacterLog: getCurrentCharacterLog, - initPageScroll: initPageScroll, + findInViewport: findInViewport, + initScrollSpy: initScrollSpy, + getConfirmationTemplate: getConfirmationTemplate, convertXEditableOptionsToSelect2: convertXEditableOptionsToSelect2, flattenXEditableSelectArray: flattenXEditableSelectArray, getCharacterDataBySystemId: getCharacterDataBySystemId, @@ -3307,6 +3555,7 @@ define([ getBrowserTabId: getBrowserTabId, singleDoubleClick: singleDoubleClick, getTableId: getTableId, + getTableRowId: getTableRowId, getDataTableInstance: getDataTableInstance, htmlEncode: htmlEncode, htmlDecode: htmlDecode, diff --git a/js/lib/bootbox.min.js b/js/lib/bootbox.min.js index cb8edd0a..908c73fe 100644 --- a/js/lib/bootbox.min.js +++ b/js/lib/bootbox.min.js @@ -1,6 +1,6 @@ /** - * bootbox.js v4.4.0 + * bootbox.js 5.2.0 * * http://bootboxjs.com/license.txt */ -!function(a,b){"use strict";"function"==typeof define&&define.amd?define(["jquery"],b):"object"==typeof exports?module.exports=b(require("jquery")):a.bootbox=b(a.jQuery)}(this,function a(b,c){"use strict";function d(a){var b=q[o.locale];return b?b[a]:q.en[a]}function e(a,c,d){a.stopPropagation(),a.preventDefault();var e=b.isFunction(d)&&d.call(c,a)===!1;e||c.modal("hide")}function f(a){var b,c=0;for(b in a)c++;return c}function g(a,c){var d=0;b.each(a,function(a,b){c(a,b,d++)})}function h(a){var c,d;if("object"!=typeof a)throw new Error("Please supply an object of options");if(!a.message)throw new Error("Please specify a message");return a=b.extend({},o,a),a.buttons||(a.buttons={}),c=a.buttons,d=f(c),g(c,function(a,e,f){if(b.isFunction(e)&&(e=c[a]={callback:e}),"object"!==b.type(e))throw new Error("button with key "+a+" must be an object");e.label||(e.label=a),e.className||(e.className=2>=d&&f===d-1?"btn-primary":"btn-default")}),a}function i(a,b){var c=a.length,d={};if(1>c||c>2)throw new Error("Invalid argument length");return 2===c||"string"==typeof a[0]?(d[b[0]]=a[0],d[b[1]]=a[1]):d=a[0],d}function j(a,c,d){return b.extend(!0,{},a,i(c,d))}function k(a,b,c,d){var e={className:"bootbox-"+a,buttons:l.apply(null,b)};return m(j(e,d,c),b)}function l(){for(var a={},b=0,c=arguments.length;c>b;b++){var e=arguments[b],f=e.toLowerCase(),g=e.toUpperCase();a[f]={label:d(g)}}return a}function m(a,b){var d={};return g(b,function(a,b){d[b]=!0}),g(a.buttons,function(a){if(d[a]===c)throw new Error("button key "+a+" is not allowed (options are "+b.join("\n")+")")}),a}var n={dialog:"",header:"",footer:"",closeButton:"",form:"",inputs:{text:"",textarea:"",email:"",select:"",checkbox:"
",date:"",time:"",number:"",password:""}},o={locale:"en",backdrop:"static",animate:!0,className:null,closeButton:!0,show:!0,container:"body"},p={};p.alert=function(){var a;if(a=k("alert",["ok"],["message","callback"],arguments),a.callback&&!b.isFunction(a.callback))throw new Error("alert requires callback property to be a function when provided");return a.buttons.ok.callback=a.onEscape=function(){return b.isFunction(a.callback)?a.callback.call(this):!0},p.dialog(a)},p.confirm=function(){var a;if(a=k("confirm",["cancel","confirm"],["message","callback"],arguments),a.buttons.cancel.callback=a.onEscape=function(){return a.callback.call(this,!1)},a.buttons.confirm.callback=function(){return a.callback.call(this,!0)},!b.isFunction(a.callback))throw new Error("confirm requires a callback");return p.dialog(a)},p.prompt=function(){var a,d,e,f,h,i,k;if(f=b(n.form),d={className:"bootbox-prompt",buttons:l("cancel","confirm"),value:"",inputType:"text"},a=m(j(d,arguments,["title","callback"]),["cancel","confirm"]),i=a.show===c?!0:a.show,a.message=f,a.buttons.cancel.callback=a.onEscape=function(){return a.callback.call(this,null)},a.buttons.confirm.callback=function(){var c;switch(a.inputType){case"text":case"textarea":case"email":case"select":case"date":case"time":case"number":case"password":c=h.val();break;case"checkbox":var d=h.find("input:checked");c=[],g(d,function(a,d){c.push(b(d).val())})}return a.callback.call(this,c)},a.show=!1,!a.title)throw new Error("prompt requires a title");if(!b.isFunction(a.callback))throw new Error("prompt requires a callback");if(!n.inputs[a.inputType])throw new Error("invalid prompt type");switch(h=b(n.inputs[a.inputType]),a.inputType){case"text":case"textarea":case"email":case"date":case"time":case"number":case"password":h.val(a.value);break;case"select":var o={};if(k=a.inputOptions||[],!b.isArray(k))throw new Error("Please pass an array of input options");if(!k.length)throw new Error("prompt with select requires options");g(k,function(a,d){var e=h;if(d.value===c||d.text===c)throw new Error("given options in wrong format");d.group&&(o[d.group]||(o[d.group]=b("").attr("label",d.group)),e=o[d.group]),e.append("")}),g(o,function(a,b){h.append(b)}),h.val(a.value);break;case"checkbox":var q=b.isArray(a.value)?a.value:[a.value];if(k=a.inputOptions||[],!k.length)throw new Error("prompt with checkbox requires options");if(!k[0].value||!k[0].text)throw new Error("given options in wrong format");h=b("
"),g(k,function(c,d){var e=b(n.inputs[a.inputType]);e.find("input").attr("value",d.value),e.find("label").append(d.text),g(q,function(a,b){b===d.value&&e.find("input").prop("checked",!0)}),h.append(e)})}return a.placeholder&&h.attr("placeholder",a.placeholder),a.pattern&&h.attr("pattern",a.pattern),a.maxlength&&h.attr("maxlength",a.maxlength),f.append(h),f.on("submit",function(a){a.preventDefault(),a.stopPropagation(),e.find(".btn-primary").click()}),e=p.dialog(a),e.off("shown.bs.modal"),e.on("shown.bs.modal",function(){h.focus()}),i===!0&&e.modal("show"),e},p.dialog=function(a){a=h(a);var d=b(n.dialog),f=d.find(".modal-dialog"),i=d.find(".modal-body"),j=a.buttons,k="",l={onEscape:a.onEscape};if(b.fn.modal===c)throw new Error("$.fn.modal is not defined; please double check you have included the Bootstrap JavaScript library. See http://getbootstrap.com/javascript/ for more details.");if(g(j,function(a,b){k+="",l[a]=b.callback}),i.find(".bootbox-body").html(a.message),a.animate===!0&&d.addClass("fade"),a.className&&d.addClass(a.className),"large"===a.size?f.addClass("modal-lg"):"small"===a.size&&f.addClass("modal-sm"),a.title&&i.before(n.header),a.closeButton){var m=b(n.closeButton);a.title?d.find(".modal-header").prepend(m):m.prependTo(i)}return a.title&&d.find(".modal-title").html(a.title),k.length&&(i.after(n.footer),d.find(".modal-footer").html(k)),d.on("hidden.bs.modal",function(a){a.target===this&&d.remove()}),d.on("shown.bs.modal",function(){d.find(".btn-primary:first").focus()}),"static"!==a.backdrop&&d.on("click.dismiss.bs.modal",function(a){d.children(".modal-backdrop").length&&(a.currentTarget=d.children(".modal-backdrop").get(0)),a.target===a.currentTarget&&d.trigger("escape.close.bb")}),d.on("escape.close.bb",function(a){l.onEscape&&e(a,d,l.onEscape)}),d.on("click",".modal-footer button",function(a){var c=b(this).data("bb-handler");e(a,d,l[c])}),d.on("click",".bootbox-close-button",function(a){e(a,d,l.onEscape)}),d.on("keyup",function(a){27===a.which&&d.trigger("escape.close.bb")}),b(a.container).append(d),d.modal({backdrop:a.backdrop?"static":!1,keyboard:!1,show:!1}),a.show&&d.modal("show"),d},p.setDefaults=function(){var a={};2===arguments.length?a[arguments[0]]=arguments[1]:a=arguments[0],b.extend(o,a)},p.hideAll=function(){return b(".bootbox").modal("hide"),p};var q={bg_BG:{OK:"Ок",CANCEL:"Отказ",CONFIRM:"Потвърждавам"},br:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Sim"},cs:{OK:"OK",CANCEL:"Zrušit",CONFIRM:"Potvrdit"},da:{OK:"OK",CANCEL:"Annuller",CONFIRM:"Accepter"},de:{OK:"OK",CANCEL:"Abbrechen",CONFIRM:"Akzeptieren"},el:{OK:"Εντάξει",CANCEL:"Ακύρωση",CONFIRM:"Επιβεβαίωση"},en:{OK:"OK",CANCEL:"Cancel",CONFIRM:"OK"},es:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Aceptar"},et:{OK:"OK",CANCEL:"Katkesta",CONFIRM:"OK"},fa:{OK:"قبول",CANCEL:"لغو",CONFIRM:"تایید"},fi:{OK:"OK",CANCEL:"Peruuta",CONFIRM:"OK"},fr:{OK:"OK",CANCEL:"Annuler",CONFIRM:"D'accord"},he:{OK:"אישור",CANCEL:"ביטול",CONFIRM:"אישור"},hu:{OK:"OK",CANCEL:"Mégsem",CONFIRM:"Megerősít"},hr:{OK:"OK",CANCEL:"Odustani",CONFIRM:"Potvrdi"},id:{OK:"OK",CANCEL:"Batal",CONFIRM:"OK"},it:{OK:"OK",CANCEL:"Annulla",CONFIRM:"Conferma"},ja:{OK:"OK",CANCEL:"キャンセル",CONFIRM:"確認"},lt:{OK:"Gerai",CANCEL:"Atšaukti",CONFIRM:"Patvirtinti"},lv:{OK:"Labi",CANCEL:"Atcelt",CONFIRM:"Apstiprināt"},nl:{OK:"OK",CANCEL:"Annuleren",CONFIRM:"Accepteren"},no:{OK:"OK",CANCEL:"Avbryt",CONFIRM:"OK"},pl:{OK:"OK",CANCEL:"Anuluj",CONFIRM:"Potwierdź"},pt:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Confirmar"},ru:{OK:"OK",CANCEL:"Отмена",CONFIRM:"Применить"},sq:{OK:"OK",CANCEL:"Anulo",CONFIRM:"Prano"},sv:{OK:"OK",CANCEL:"Avbryt",CONFIRM:"OK"},th:{OK:"ตกลง",CANCEL:"ยกเลิก",CONFIRM:"ยืนยัน"},tr:{OK:"Tamam",CANCEL:"İptal",CONFIRM:"Onayla"},zh_CN:{OK:"OK",CANCEL:"取消",CONFIRM:"确认"},zh_TW:{OK:"OK",CANCEL:"取消",CONFIRM:"確認"}};return p.addLocale=function(a,c){return b.each(["OK","CANCEL","CONFIRM"],function(a,b){if(!c[b])throw new Error("Please supply a translation for '"+b+"'")}),q[a]={OK:c.OK,CANCEL:c.CANCEL,CONFIRM:c.CONFIRM},p},p.removeLocale=function(a){return delete q[a],p},p.setLocale=function(a){return p.setDefaults("locale",a)},p.init=function(c){return a(c||b)},p}); \ No newline at end of file +!function(e,t){'use strict';'function'==typeof define&&define.amd?define(['jquery'],t):'object'==typeof exports?module.exports=t(require('jquery')):e.bootbox=t(e.jQuery)}(this,function t(p,u){'use strict';var r,n,i,l;Object.keys||(Object.keys=(r=Object.prototype.hasOwnProperty,n=!{toString:null}.propertyIsEnumerable('toString'),l=(i=['toString','toLocaleString','valueOf','hasOwnProperty','isPrototypeOf','propertyIsEnumerable','constructor']).length,function(e){if('function'!=typeof e&&('object'!=typeof e||null===e))throw new TypeError('Object.keys called on non-object');var t,o,a=[];for(t in e)r.call(e,t)&&a.push(t);if(n)for(o=0;o
",header:"
",footer:'',closeButton:'',form:'
',button:'',option:'',promptMessage:'
',inputs:{text:'',textarea:'',email:'',select:'',checkbox:'
',radio:'
',date:'',time:'',number:'',password:'',range:''}},m={locale:'en',backdrop:'static',animate:!0,className:null,closeButton:!0,show:!0,container:'body',value:'',inputType:'text',swapButtonOrder:!1,centerVertical:!1,multiple:!1,scrollable:!1};function c(e,t,o){return p.extend(!0,{},e,function(e,t){var o=e.length,a={};if(o<1||2').attr('label',t.group)),o=i[t.group]);var a=p(f.option);a.attr('value',t.value).text(t.text),o.append(a)}),v(i,function(e,t){n.append(t)}),n.val(r.value);break;case'checkbox':var l=p.isArray(r.value)?r.value:[r.value];if(!(a=r.inputOptions||[]).length)throw new Error('prompt with "inputType" set to "checkbox" requires at least one option');n=p('
'),v(a,function(e,o){if(o.value===u||o.text===u)throw new Error('each option needs a "value" property and a "text" property');var a=p(f.inputs[r.inputType]);a.find('input').attr('value',o.value),a.find('label').append('\n'+o.text),v(l,function(e,t){t===o.value&&a.find('input').prop('checked',!0)}),n.append(a)});break;case'radio':if(r.value!==u&&p.isArray(r.value))throw new Error('prompt with "inputType" set to "radio" requires a single, non-array value for "value"');if(!(a=r.inputOptions||[]).length)throw new Error('prompt with "inputType" set to "radio" requires at least one option');n=p('
');var s=!0;v(a,function(e,t){if(t.value===u||t.text===u)throw new Error('each option needs a "value" property and a "text" property');var o=p(f.inputs[r.inputType]);o.find('input').attr('value',t.value),o.find('label').append('\n'+t.text),r.value!==u&&t.value===r.value&&(o.find('input').prop('checked',!0),s=!1),n.append(o)}),s&&n.find('input[type="radio"]').first().prop('checked',!0)}if(e.append(n),e.on('submit',function(e){e.preventDefault(),e.stopPropagation(),t.find('.bootbox-accept').trigger('click')}),''!==p.trim(r.message)){var c=p(f.promptMessage).html(r.message);e.prepend(c),r.message=e}else r.message=e;return(t=d.dialog(r)).off('shown.bs.modal'),t.on('shown.bs.modal',function(){n.focus()}),!0===o&&t.modal('show'),t},d.addLocale('en',{OK:'OK',CANCEL:'Cancel',CONFIRM:'OK'}),d}); \ No newline at end of file diff --git a/js/lib/jquery-3.3.1.min.js b/js/lib/jquery-3.3.1.min.js deleted file mode 100644 index 49d1fcfb..00000000 --- a/js/lib/jquery-3.3.1.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var b="3.3.1",w=function(e,t){return new w.fn.init(e,t)},T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:"3.3.1",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b="sizzle"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n+~]|"+M+")"+M+"*"),z=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),X=new RegExp(W),U=new RegExp("^"+R+"$"),V={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+P+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},G=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/[+~]/,Z=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ee=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ne=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){p()},ie=me(function(e){return!0===e.disabled&&("form"in e||"label"in e)},{dir:"parentNode",next:"legend"});try{L.apply(A=H.call(w.childNodes),w.childNodes),A[w.childNodes.length].nodeType}catch(e){L={apply:A.length?function(e,t){q.apply(e,H.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function oe(e,t,r,i){var o,s,l,c,f,h,v,m=t&&t.ownerDocument,T=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==T&&9!==T&&11!==T)return r;if(!i&&((t?t.ownerDocument||t:w)!==d&&p(t),t=t||d,g)){if(11!==T&&(f=J.exec(e)))if(o=f[1]){if(9===T){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return L.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return L.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!S[e+" "]&&(!y||!y.test(e))){if(1!==T)m=t,v=e;else if("object"!==t.nodeName.toLowerCase()){(c=t.getAttribute("id"))?c=c.replace(te,ne):t.setAttribute("id",c=b),s=(h=a(e)).length;while(s--)h[s]="#"+c+" "+ve(h[s]);v=h.join(","),m=K.test(e)&&ge(t.parentNode)||t}if(v)try{return L.apply(r,m.querySelectorAll(v)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(B,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function se(e){return e[b]=!0,e}function ue(e){var t=d.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function fe(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function pe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ie(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return se(function(t){return t=+t,se(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}n=oe.support={},o=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},p=oe.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==d&&9===a.nodeType&&a.documentElement?(d=a,h=d.documentElement,g=!o(d),w!==d&&(i=d.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ue(function(e){return e.appendChild(d.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(d.getElementsByClassName),n.getById=ue(function(e){return h.appendChild(e).id=b,!d.getElementsByName||!d.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],y=[],(n.qsa=Q.test(d.querySelectorAll))&&(ue(function(e){h.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+P+")"),e.querySelectorAll("[id~="+b+"-]").length||y.push("~="),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||y.push(".#.+[+~]")}),ue(function(e){e.innerHTML="";var t=d.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(n.matchesSelector=Q.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ue(function(e){n.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),v.push("!=",W)}),y=y.length&&new RegExp(y.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===d||e.ownerDocument===w&&x(w,e)?-1:t===d||t.ownerDocument===w&&x(w,t)?1:c?O(c,e)-O(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===d?-1:t===d?1:i?-1:o?1:c?O(c,e)-O(c,t):0;if(i===o)return ce(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?ce(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},d):d},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==d&&p(e),t=t.replace(z,"='$1']"),n.matchesSelector&&g&&!S[t+" "]&&(!v||!v.test(t))&&(!y||!y.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return oe(t,d,null,[e]).length>0},oe.contains=function(e,t){return(e.ownerDocument||e)!==d&&p(e),x(e,t)},oe.attr=function(e,t){(e.ownerDocument||e)!==d&&p(e);var i=r.attrHandle[t.toLowerCase()],o=i&&N.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},oe.escape=function(e){return(e+"").replace(te,ne)},oe.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},oe.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(D),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return c=null,e},i=oe.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=oe.selectors={cacheLength:50,createPseudo:se,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(Z,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return V.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=oe.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace($," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==a?"nextSibling":"previousSibling",y=t.parentNode,v=s&&t.nodeName.toLowerCase(),m=!u&&!s,x=!1;if(y){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===v:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?y.firstChild:y.lastChild],a&&m){x=(d=(l=(c=(f=(p=y)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],p=d&&y.childNodes[d];while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if(1===p.nodeType&&++x&&p===t){c[e]=[T,d,x];break}}else if(m&&(x=d=(l=(c=(f=(p=t)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===v:1===p.nodeType)&&++x&&(m&&((c=(f=p[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]=[T,x]),p===t))break;return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||oe.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?se(function(e,n){var r,o=i(e,t),a=o.length;while(a--)e[r=O(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:se(function(e){var t=[],n=[],r=s(e.replace(B,"$1"));return r[b]?se(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:se(function(e){return function(t){return oe(e,t).length>0}}),contains:se(function(e){return e=e.replace(Z,ee),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:se(function(e){return U.test(e||"")||oe.error("unsupported lang: "+e),e=e.replace(Z,ee).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:de(!1),disabled:de(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r-1&&(o[l]=!(a[l]=f))}}else v=we(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):L.apply(a,v)})}function Ce(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return O(t,e)>-1},s,!0),p=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u1&&xe(p),u>1&&ve(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(B,"$1"),n,u0,i=e.length>0,o=function(o,a,s,u,c){var f,h,y,v=0,m="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,k=C.length;for(c&&(l=a===d||a||c);m!==k&&null!=(f=C[m]);m++){if(i&&f){h=0,a||f.ownerDocument===d||(p(f),s=!g);while(y=e[h++])if(y(f,a||d,s)){u.push(f);break}c&&(T=E)}n&&((f=!y&&f)&&v--,o&&x.push(f))}if(v+=m,n&&m!==v){h=0;while(y=t[h++])y(x,b,a,s);if(o){if(v>0)while(m--)x[m]||b[m]||(b[m]=j.call(u));b=we(b)}L.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&oe.uniqueSort(u)}return c&&(T=E,l=w),x};return n?se(o):o}return s=oe.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=a(e)),n=t.length;while(n--)(o=Ce(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},u=oe.select=function(e,t,n,i){var o,u,l,c,f,p="function"==typeof e&&e,d=!i&&a(e=p.selector||e);if(n=n||[],1===d.length){if((u=d[0]=d[0].slice(0)).length>2&&"ID"===(l=u[0]).type&&9===t.nodeType&&g&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(Z,ee),t)||[])[0]))return n;p&&(t=t.parentNode),e=e.slice(u.shift().value.length)}o=V.needsContext.test(e)?0:u.length;while(o--){if(l=u[o],r.relative[c=l.type])break;if((f=r.find[c])&&(i=f(l.matches[0].replace(Z,ee),K.test(u[0].type)&&ge(t.parentNode)||t))){if(u.splice(o,1),!(e=i.length&&ve(u)))return L.apply(n,i),n;break}}}return(p||s(e,d))(i,t,!g,n,!t||K.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(D).join("")===b,n.detectDuplicates=!!f,p(),n.sortDetached=ue(function(e){return 1&e.compareDocumentPosition(d.createElement("fieldset"))}),ue(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||le("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ue(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||le("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ue(function(e){return null==e.getAttribute("disabled")})||le(P,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(e);w.find=E,w.expr=E.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=E.uniqueSort,w.text=E.getText,w.isXMLDoc=E.isXML,w.contains=E.contains,w.escapeSelector=E.escape;var k=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},D=w.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,t,n){return g(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return u.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&D.test(e)?w(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:L.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),A.test(i[1])&&w.isPlainObject(t))for(i in t)g(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=r.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,q=w(r);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&w.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?w.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?u.call(w(e),this[0]):u.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return k(e,"parentNode")},parentsUntil:function(e,t,n){return k(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return k(e,"nextSibling")},prevAll:function(e){return k(e,"previousSibling")},nextUntil:function(e,t,n){return k(e,"nextSibling",n)},prevUntil:function(e,t,n){return k(e,"previousSibling",n)},siblings:function(e){return S((e.parentNode||{}).firstChild,e)},children:function(e){return S(e.firstChild)},contents:function(e){return N(e,"iframe")?e.contentDocument:(N(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=w.filter(r,i)),this.length>1&&(O[e]||w.uniqueSort(i),H.test(e)&&i.reverse()),this.pushStack(i)}});var M=/[^\x20\t\r\n\f]+/g;function R(e){var t={};return w.each(e.match(M)||[],function(e,n){t[n]=!0}),t}w.Callbacks=function(e){e="string"==typeof e?R(e):w.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;a.length;s=-1){n=a.shift();while(++s-1)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l};function I(e){return e}function W(e){throw e}function $(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=g(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t=o&&(r!==W&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(w.Deferred.getStackHook&&(c.stackTrace=w.Deferred.getStackHook()),e.setTimeout(c))}}return w.Deferred(function(e){n[0][3].add(a(0,e,g(i)?i:I,e.notifyWith)),n[1][3].add(a(0,e,g(t)?t:I)),n[2][3].add(a(0,e,g(r)?r:W))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},o={};return w.each(n,function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=o.call(arguments),a=w.Deferred(),s=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?o.call(arguments):n,--t||a.resolveWith(r,i)}};if(t<=1&&($(e,a.done(s(n)).resolve,a.reject,!t),"pending"===a.state()||g(i[n]&&i[n].then)))return a.then();while(n--)$(i[n],s(n),a.reject);return a.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&B.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var F=w.Deferred();w.fn.ready=function(e){return F.then(e)["catch"](function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||F.resolveWith(r,[w]))}}),w.ready.then=F.then;function _(){r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_),w.ready()}"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_));var z=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===x(n)){i=!0;for(s in n)z(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,g(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(w(e),n)})),t))for(;s1,null,!0)},removeData:function(e){return this.each(function(){K.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=J.get(e,t),n&&(!r||Array.isArray(n)?r=J.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,i=n.shift(),o=w._queueHooks(e,t),a=function(){w.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return J.get(e,n)||J.access(e,n,{empty:w.Callbacks("once memory").add(function(){J.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"
'; - table += 'Region'; - table += ''; - table += regionName; - table += '
'; - table += 'Faction'; + table += rowData.label; table += ''; - table += faction.name; + table += rowData.value; table += '
","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&N(e,t)?w.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n-1)i&&i.push(o);else if(l=w.contains(o.ownerDocument,o),a=ye(f.appendChild(o),"script"),l&&ve(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var be=r.documentElement,we=/^key/,Te=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function ke(){return!1}function Se(){try{return r.activeElement}catch(e){}}function De(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)De(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.get(e);if(y){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(be,i),n.guid||(n.guid=w.guid++),(u=y.events)||(u=y.events={}),(a=y.handle)||(a=y.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(M)||[""]).length;while(l--)d=g=(s=Ce.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=w.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=w.event.special[d]||{},c=w.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),w.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.hasData(e)&&J.get(e);if(y&&(u=y.events)){l=(t=(t||"").match(M)||[""]).length;while(l--)if(s=Ce.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){f=w.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,y.handle)||w.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)w.event.remove(e,d+t[l],n,r,!0);w.isEmptyObject(u)&&J.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,s,u=new Array(arguments.length),l=(J.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(u[0]=t,n=1;n=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/\s*$/g;function Le(e,t){return N(e,"table")&&N(11!==t.nodeType?t:t.firstChild,"tr")?w(e).children("tbody")[0]||e:e}function He(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Oe(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,i,o,a,s,u,l;if(1===t.nodeType){if(J.hasData(e)&&(o=J.access(e),a=J.set(t,o),l=o.events)){delete a.handle,a.events={};for(i in l)for(n=0,r=l[i].length;n1&&"string"==typeof y&&!h.checkClone&&je.test(y))return e.each(function(i){var o=e.eq(i);v&&(t[0]=y.call(this,i,o.html())),Re(o,t,n,r)});if(p&&(i=xe(t,e[0].ownerDocument,!1,e,r),o=i.firstChild,1===i.childNodes.length&&(i=o),o||r)){for(u=(s=w.map(ye(i,"script"),He)).length;f")},clone:function(e,t,n){var r,i,o,a,s=e.cloneNode(!0),u=w.contains(e.ownerDocument,e);if(!(h.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||w.isXMLDoc(e)))for(a=ye(s),r=0,i=(o=ye(e)).length;r0&&ve(a,!u&&ye(e,"script")),s},cleanData:function(e){for(var t,n,r,i=w.event.special,o=0;void 0!==(n=e[o]);o++)if(Y(n)){if(t=n[J.expando]){if(t.events)for(r in t.events)i[r]?w.event.remove(n,r):w.removeEvent(n,r,t.handle);n[J.expando]=void 0}n[K.expando]&&(n[K.expando]=void 0)}}}),w.fn.extend({detach:function(e){return Ie(this,e,!0)},remove:function(e){return Ie(this,e)},text:function(e){return z(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||Le(this,e).appendChild(e)})},prepend:function(){return Re(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Le(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(ye(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 z(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&&!Ae.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))),u}function et(e,t,n){var r=$e(e),i=Fe(e,t,r),o="border-box"===w.css(e,"boxSizing",!1,r),a=o;if(We.test(i)){if(!n)return i;i="auto"}return a=a&&(h.boxSizingReliable()||i===e.style[t]),("auto"===i||!parseFloat(i)&&"inline"===w.css(e,"display",!1,r))&&(i=e["offset"+t[0].toUpperCase()+t.slice(1)],a=!0),(i=parseFloat(i)||0)+Ze(e,t,n||(o?"border":"content"),a,r,i)+"px"}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Fe(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 i,o,a,s=G(t),u=Xe.test(t),l=e.style;if(u||(t=Je(s)),a=w.cssHooks[t]||w.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"==(o=typeof n)&&(i=ie.exec(n))&&i[1]&&(n=ue(e,t,i),o="number"),null!=n&&n===n&&("number"===o&&(n+=i&&i[3]||(w.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=G(t);return Xe.test(t)||(t=Je(s)),(a=w.cssHooks[t]||w.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Fe(e,t,r)),"normal"===i&&t in Ve&&(i=Ve[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),w.each(["height","width"],function(e,t){w.cssHooks[t]={get:function(e,n,r){if(n)return!ze.test(w.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?et(e,t,r):se(e,Ue,function(){return et(e,t,r)})},set:function(e,n,r){var i,o=$e(e),a="border-box"===w.css(e,"boxSizing",!1,o),s=r&&Ze(e,t,r,a,o);return a&&h.scrollboxSize()===o.position&&(s-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(o[t])-Ze(e,t,"border",!1,o)-.5)),s&&(i=ie.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=w.css(e,t)),Ke(e,n,s)}}}),w.cssHooks.marginLeft=_e(h.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Fe(e,"marginLeft"))||e.getBoundingClientRect().left-se(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,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+oe[r]+t]=o[r]||o[r-2]||o[0];return i}},"margin"!==e&&(w.cssHooks[e+t].set=Ke)}),w.fn.extend({css:function(e,t){return z(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=$e(e),i=t.length;a1)}});function tt(e,t,n,r,i){return new tt.prototype.init(e,t,n,r,i)}w.Tween=tt,tt.prototype={constructor:tt,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(w.cssNumber[n]?"":"px")},cur:function(){var e=tt.propHooks[this.prop];return e&&e.get?e.get(this):tt.propHooks._default.get(this)},run:function(e){var t,n=tt.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):tt.propHooks._default.set(this),this}},tt.prototype.init.prototype=tt.prototype,tt.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)}}},tt.propHooks.scrollTop=tt.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=tt.prototype.init,w.fx.step={};var nt,rt,it=/^(?:toggle|show|hide)$/,ot=/queueHooks$/;function at(){rt&&(!1===r.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(at):e.setTimeout(at,w.fx.interval),w.fx.tick())}function st(){return e.setTimeout(function(){nt=void 0}),nt=Date.now()}function ut(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=oe[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function lt(e,t,n){for(var r,i=(pt.tweeners[t]||[]).concat(pt.tweeners["*"]),o=0,a=i.length;o1)},removeAttr:function(e){return this.each(function(){w.removeAttr(this,e)})}}),w.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?w.prop(e,t,n):(1===o&&w.isXMLDoc(e)||(i=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?dt:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.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&&N(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(M);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),dt={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=ht[t]||w.find.attr;ht[t]=function(e,t,r){var i,o,a=t.toLowerCase();return r||(o=ht[a],ht[a]=i,i=null!=n(e,t,r)?a:null,ht[a]=o),i}});var gt=/^(?:input|select|textarea|button)$/i,yt=/^(?:a|area)$/i;w.fn.extend({prop:function(e,t){return z(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,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&w.isXMLDoc(e)||(t=w.propFix[t]||t,i=w.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,"tabindex");return t?parseInt(t,10):gt.test(e.nodeName)||yt.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});function vt(e){return(e.match(M)||[]).join(" ")}function mt(e){return e.getAttribute&&e.getAttribute("class")||""}function xt(e){return Array.isArray(e)?e:"string"==typeof e?e.match(M)||[]:[]}w.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).addClass(e.call(this,t,mt(this)))});if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).removeClass(e.call(this,t,mt(this)))});if(!arguments.length)return this.attr("class","");if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])while(r.indexOf(" "+o+" ")>-1)r=r.replace(" "+o+" "," ");i!==(s=vt(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):g(e)?this.each(function(n){w(this).toggleClass(e.call(this,n,mt(this),t),t)}):this.each(function(){var t,i,o,a;if(r){i=0,o=w(this),a=xt(e);while(t=a[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else void 0!==e&&"boolean"!==n||((t=mt(this))&&J.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":J.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&(" "+vt(mt(n))+" ").indexOf(t)>-1)return!0;return!1}});var bt=/\r/g;w.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=g(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,w(this).val()):e)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=w.map(i,function(e){return null==e?"":e+""})),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return(t=w.valHooks[i.type]||w.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(bt,""):null==n?"":n}}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,"value");return null!=t?t:vt(w.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),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 wt=/^(?:focusinfocus|focusoutblur)$/,Tt=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(t,n,i,o){var a,s,u,l,c,p,d,h,v=[i||r],m=f.call(t,"type")?t.type:t,x=f.call(t,"namespace")?t.namespace.split("."):[];if(s=h=u=i=i||r,3!==i.nodeType&&8!==i.nodeType&&!wt.test(m+w.event.triggered)&&(m.indexOf(".")>-1&&(m=(x=m.split(".")).shift(),x.sort()),c=m.indexOf(":")<0&&"on"+m,t=t[w.expando]?t:new w.Event(m,"object"==typeof t&&t),t.isTrigger=o?2:3,t.namespace=x.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+x.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),n=null==n?[t]:w.makeArray(n,[t]),d=w.event.special[m]||{},o||!d.trigger||!1!==d.trigger.apply(i,n))){if(!o&&!d.noBubble&&!y(i)){for(l=d.delegateType||m,wt.test(l+m)||(s=s.parentNode);s;s=s.parentNode)v.push(s),u=s;u===(i.ownerDocument||r)&&v.push(u.defaultView||u.parentWindow||e)}a=0;while((s=v[a++])&&!t.isPropagationStopped())h=s,t.type=a>1?l:d.bindType||m,(p=(J.get(s,"events")||{})[t.type]&&J.get(s,"handle"))&&p.apply(s,n),(p=c&&s[c])&&p.apply&&Y(s)&&(t.result=p.apply(s,n),!1===t.result&&t.preventDefault());return t.type=m,o||t.isDefaultPrevented()||d._default&&!1!==d._default.apply(v.pop(),n)||!Y(i)||c&&g(i[m])&&!y(i)&&((u=i[c])&&(i[c]=null),w.event.triggered=m,t.isPropagationStopped()&&h.addEventListener(m,Tt),i[m](),t.isPropagationStopped()&&h.removeEventListener(m,Tt),w.event.triggered=void 0,u&&(i[c]=u)),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,i=J.access(r,t);i||r.addEventListener(e,n,!0),J.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=J.access(r,t)-1;i?J.access(r,t,i):(r.removeEventListener(e,n,!0),J.remove(r,t))}}});var Ct=e.location,Et=Date.now(),kt=/\?/;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 St=/\[\]$/,Dt=/\r?\n/g,Nt=/^(?:submit|button|image|reset|file)$/i,At=/^(?:input|select|textarea|keygen)/i;function jt(e,t,n,r){var i;if(Array.isArray(t))w.each(t,function(t,i){n||St.test(e)?r(e,i):jt(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)});else if(n||"object"!==x(t))r(e,t);else for(i in t)jt(e+"["+i+"]",t[i],n,r)}w.param=function(e,t){var n,r=[],i=function(e,t){var n=g(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(){i(this.name,this.value)});else for(n in e)jt(n,e[n],t,i);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")&&At.test(this.nodeName)&&!Nt.test(e)&&(this.checked||!pe.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(Dt,"\r\n")}}):{name:t.name,value:n.replace(Dt,"\r\n")}}).get()}});var qt=/%20/g,Lt=/#.*$/,Ht=/([?&])_=[^&]*/,Ot=/^(.*?):[ \t]*([^\r\n]*)$/gm,Pt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Mt=/^(?:GET|HEAD)$/,Rt=/^\/\//,It={},Wt={},$t="*/".concat("*"),Bt=r.createElement("a");Bt.href=Ct.href;function Ft(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(M)||[];if(g(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function _t(e,t,n,r){var i={},o=e===Wt;function a(s){var u;return i[s]=!0,w.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function zt(e,t){var n,r,i=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&w.extend(!0,e,r),e}function Xt(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}function Ut(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ct.href,type:"GET",isLocal:Pt.test(Ct.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":$t,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?zt(zt(e,w.ajaxSettings),t):zt(w.ajaxSettings,e)},ajaxPrefilter:Ft(It),ajaxTransport:Ft(Wt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var i,o,a,s,u,l,c,f,p,d,h=w.ajaxSetup({},n),g=h.context||h,y=h.context&&(g.nodeType||g.jquery)?w(g):w.event,v=w.Deferred(),m=w.Callbacks("once memory"),x=h.statusCode||{},b={},T={},C="canceled",E={readyState:0,getResponseHeader:function(e){var t;if(c){if(!s){s={};while(t=Ot.exec(a))s[t[1].toLowerCase()]=t[2]}t=s[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return c?a:null},setRequestHeader:function(e,t){return null==c&&(e=T[e.toLowerCase()]=T[e.toLowerCase()]||e,b[e]=t),this},overrideMimeType:function(e){return null==c&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(c)E.always(e[E.status]);else for(t in e)x[t]=[x[t],e[t]];return this},abort:function(e){var t=e||C;return i&&i.abort(t),k(0,t),this}};if(v.promise(E),h.url=((t||h.url||Ct.href)+"").replace(Rt,Ct.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(M)||[""],null==h.crossDomain){l=r.createElement("a");try{l.href=h.url,l.href=l.href,h.crossDomain=Bt.protocol+"//"+Bt.host!=l.protocol+"//"+l.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=w.param(h.data,h.traditional)),_t(It,h,n,E),c)return E;(f=w.event&&h.global)&&0==w.active++&&w.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Mt.test(h.type),o=h.url.replace(Lt,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(qt,"+")):(d=h.url.slice(o.length),h.data&&(h.processData||"string"==typeof h.data)&&(o+=(kt.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(Ht,"$1"),d=(kt.test(o)?"&":"?")+"_="+Et+++d),h.url=o+d),h.ifModified&&(w.lastModified[o]&&E.setRequestHeader("If-Modified-Since",w.lastModified[o]),w.etag[o]&&E.setRequestHeader("If-None-Match",w.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&E.setRequestHeader("Content-Type",h.contentType),E.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+$t+"; q=0.01":""):h.accepts["*"]);for(p in h.headers)E.setRequestHeader(p,h.headers[p]);if(h.beforeSend&&(!1===h.beforeSend.call(g,E,h)||c))return E.abort();if(C="abort",m.add(h.complete),E.done(h.success),E.fail(h.error),i=_t(Wt,h,n,E)){if(E.readyState=1,f&&y.trigger("ajaxSend",[E,h]),c)return E;h.async&&h.timeout>0&&(u=e.setTimeout(function(){E.abort("timeout")},h.timeout));try{c=!1,i.send(b,k)}catch(e){if(c)throw e;k(-1,e)}}else k(-1,"No Transport");function k(t,n,r,s){var l,p,d,b,T,C=n;c||(c=!0,u&&e.clearTimeout(u),i=void 0,a=s||"",E.readyState=t>0?4:0,l=t>=200&&t<300||304===t,r&&(b=Xt(h,E,r)),b=Ut(h,b,E,l),l?(h.ifModified&&((T=E.getResponseHeader("Last-Modified"))&&(w.lastModified[o]=T),(T=E.getResponseHeader("etag"))&&(w.etag[o]=T)),204===t||"HEAD"===h.type?C="nocontent":304===t?C="notmodified":(C=b.state,p=b.data,l=!(d=b.error))):(d=C,!t&&C||(C="error",t<0&&(t=0))),E.status=t,E.statusText=(n||C)+"",l?v.resolveWith(g,[p,C,E]):v.rejectWith(g,[E,C,d]),E.statusCode(x),x=void 0,f&&y.trigger(l?"ajaxSuccess":"ajaxError",[E,h,l?p:d]),m.fireWith(g,[E,C]),f&&(y.trigger("ajaxComplete",[E,h]),--w.active||w.event.trigger("ajaxStop")))}return E},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,i){return g(n)&&(i=i||r,r=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:i,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]&&(g(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(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return g(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=g(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 Vt={0:200,1223:204},Gt=w.ajaxSettings.xhr();h.cors=!!Gt&&"withCredentials"in Gt,h.ajax=Gt=!!Gt,w.ajaxTransport(function(t){var n,r;if(h.cors||Gt&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");for(a in i)s.setRequestHeader(a,i[a]);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?o(0,"error"):o(s.status,s.statusText):o(Vt[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){if(e.crossDomain){var t,n;return{send:function(i,o){t=w("