diff --git a/.htaccess b/.htaccess index 150db93a..8203b787 100644 --- a/.htaccess +++ b/.htaccess @@ -14,4 +14,10 @@ RewriteCond %{REQUEST_FILENAME} !-l RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule .* index.php [L,QSA] -RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L] \ No newline at end of file +RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L] + +# PHP global Vars +php_value max_input_vars 5000 +php_value suhosin.get.max_vars 5000 +php_value suhosin.post.max_vars 5000 +php_value suhosin.request.max_vars 5000 \ No newline at end of file diff --git a/.idea/dataSources.ids b/.idea/dataSources.ids index ed70511d..f04fc18d 100644 --- a/.idea/dataSources.ids +++ b/.idea/dataSources.ids @@ -3573,6 +3573,25 @@ + + + + + + + + + + + + + + + + + + +
@@ -3590,11 +3609,16 @@ - - + + + + + + + @@ -3607,6 +3631,7 @@ +
@@ -3618,6 +3643,7 @@ + @@ -3640,24 +3666,69 @@ + + + + - + + + + + + + + - - + - + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
@@ -3669,6 +3740,7 @@ + @@ -3680,6 +3752,7 @@ + @@ -3693,12 +3766,29 @@ +
+ + + + + + + + + + + + + + + +
diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml index a8755ed6..7e688c30 100644 --- a/.idea/dataSources.xml +++ b/.idea/dataSources.xml @@ -1,6 +1,6 @@ - + mysql true diff --git a/.idea/dictionaries/exodus4d.xml b/.idea/dictionaries/exodus4d.xml index 4a428934..43f5a7ac 100644 --- a/.idea/dictionaries/exodus4d.xml +++ b/.idea/dictionaries/exodus4d.xml @@ -4,6 +4,7 @@ addclass bootbox contextmenu + cronjob crosshairs cytaclysmic dashstyle @@ -16,6 +17,7 @@ jumpbridge killboard killmail + malihu mouseover nonblock onerror diff --git a/app/config.cfg b/app/config.cfg index 6cab48f6..13448613 100644 --- a/app/config.cfg +++ b/app/config.cfg @@ -1,7 +1,7 @@ [globals] ; Verbosity level of the stack trace. Assign values between 0 to 3 for increasing verbosity levels -DEBUG = 0 +DEBUG = 1 ; If TRUE, the framework, after having logged stack trace and errors, stops execution (die without any status) when a non-fatal error is detected. HALT = FALSE diff --git a/app/main/controller/AccessController.php b/app/main/controller/AccessController.php index 2d71f048..fc734424 100644 --- a/app/main/controller/AccessController.php +++ b/app/main/controller/AccessController.php @@ -157,7 +157,7 @@ class AccessController extends Controller { */ protected function _getUser(){ - $user = Model\BasicModel::getNew('UserModel'); + $user = Model\BasicModel::getNew('UserModel', 5); $user->getById($this->f3->get('SESSION.user.id')); if($user->dry()){ diff --git a/app/main/controller/Controller.php b/app/main/controller/Controller.php index c2450784..d66f2168 100644 --- a/app/main/controller/Controller.php +++ b/app/main/controller/Controller.php @@ -74,8 +74,10 @@ class Controller { $this->f3->get('DB_PASS') ); } - $this->f3->set('DB', $db); + + // set DB timezone to UTC +00:00 (eve server time) + $this->f3->get('DB')->exec('SET @@session.time_zone = "+00:00";'); } diff --git a/app/main/controller/MapController.php b/app/main/controller/MapController.php index 28bc92bd..ac90f0b2 100644 --- a/app/main/controller/MapController.php +++ b/app/main/controller/MapController.php @@ -25,7 +25,6 @@ class MapController extends Controller { * @param $f3 */ public function showError($f3){ - print_r($f3->get('ERROR')); // set HTTP status if(!empty($f3->get('ERROR.code'))){ @@ -44,7 +43,7 @@ class MapController extends Controller { echo json_encode($errorData); }else{ - echo 'TODO: static errors'; + echo $f3->get('ERROR.text'); } die(); diff --git a/app/main/controller/api/Connection.php b/app/main/controller/api/Connection.php new file mode 100644 index 00000000..671e3fdd --- /dev/null +++ b/app/main/controller/api/Connection.php @@ -0,0 +1,68 @@ +get('POST.connectionData'); + + $user = $this->_getUser(); + + $connection = Model\BasicModel::getNew('ConnectionModel'); + $connection->getById($connectionData['id']); + $connection->setData($connectionData); + + $newConnectionData = []; + if( + $connection->isValid() && + $connection->hasAccess($user) + ){ + $connection->save(); + $newConnectionData = $connection->getData(); + } + + echo json_encode($newConnectionData); + } + + public function delete($f3){ + $connectionIds = $f3->get('POST.connectionIds'); + + $user = $this->_getUser(); + $connection = Model\BasicModel::getNew('ConnectionModel'); + + foreach($connectionIds as $connectionId){ + + $connection->getById($connectionId); + $connection->delete($user); + + $connection->reset(); + } + + echo json_encode([]); + } + +} \ No newline at end of file diff --git a/app/main/controller/api/Map.php b/app/main/controller/api/Map.php index ccc1b397..69a07ce0 100644 --- a/app/main/controller/api/Map.php +++ b/app/main/controller/api/Map.php @@ -109,6 +109,45 @@ class Map extends \Controller\AccessController { echo json_encode($initData); } + /** + * save a new map or update an existing map + * @param $f3 + */ + public function save($f3){ + + $mapData = (array)$f3->get('POST.mapData'); + + $map = Model\BasicModel::getNew('MapModel'); + $map->getById($mapData['id']); + $map->setData($mapData); + $map->save(); + + if($map->isPrivate()){ + $user = $this->_getUser(); + $map->setAccess($user); + } + + $mapData = $map->getData(); + + echo json_encode($mapData); + } + + /** + * delete a map and all dependencies + * @param $f3 + */ + public function delete($f3){ + $mapData = (array)$f3->get('POST.mapData'); + + $user = $this->_getUser(); + + $map = Model\BasicModel::getNew('MapModel'); + $map->getById($mapData['id']); + $map->delete($user); + + echo json_encode([]); + } + /** * update map data api * function is called continuously @@ -116,6 +155,10 @@ class Map extends \Controller\AccessController { */ public function updateData($f3){ + // cache time for response should be equal or less than this function is called + // security aspect + $responseTTL = 3; + $mapData = (array)$f3->get('POST.mapData'); $user = $this->_getUser(); @@ -125,28 +168,42 @@ class Map extends \Controller\AccessController { $connection = Model\BasicModel::getNew('ConnectionModel'); - foreach($mapData as $data){ $config = $data['config']; - $systems = $data['data']['systems']; - $connections = $data['data']['connections']; + $systems = []; + $connections = []; + + // check whether system data and/or connection data is send + // empty arrays are not included in ajax requests + if(array_key_exists('data', $data)){ + if(array_key_exists('systems', $data['data'])){ + $systems = $data['data']['systems']; + } + if(array_key_exists('connections', $data['data'])){ + $connections = $data['data']['connections']; + } + } // update map data --------------------------------------------- $map->getById($config['id']); - // update map on change - if( (int)$config['updated'] > strtotime($map->updated)){ - $map->setData($config); - $map->save(); + if(!$map->dry()){ + // update map on change + if( (int)$config['updated'] > strtotime($map->updated)){ + $map->setData($config); + $map->save(); + } } // get system data ----------------------------------------------- foreach($systems as $systemData){ - $system->getById($systemData['id']); - if((int)$systemData['updated'] > strtotime($system->updated)){ + if( + (int)$systemData['updated'] === 0 && + !$system->dry() + ){ $systemData['mapId'] = $map; $system->setData($systemData); $system->save(); @@ -157,15 +214,17 @@ class Map extends \Controller\AccessController { // get connection data ------------------------------------------- foreach($connections as $connectionData){ - $connection->getById($connectionData['id']); - if((int)$connectionData['updated'] > strtotime($connection->updated)){ - + if( + (int)$connectionData['updated'] === 0 && + !$connection->dry() + ){ $connectionData['mapId'] = $map; $connection->setData($connectionData); - $connection->save(); + $connection->save($user); } + $connection->reset(); } @@ -173,25 +232,41 @@ class Map extends \Controller\AccessController { } // get map data ====================================================== + $cacheKey = 'user_map_data_' . $user->id; - $activeMaps = $user->getMaps(); + if($f3->exists($cacheKey) === false){ + $activeMaps = $user->getMaps(); - $newData = []; - foreach($activeMaps as $activeMap){ + // format map Data for return + $newData = self::getFormattedMapData($activeMaps); - $newData[] = [ - 'config' => $activeMap->getData(), + $f3->set($cacheKey, $newData, $responseTTL); + }else{ + // TODO log -> manipulated request + } + + + echo json_encode( $f3->get($cacheKey) ); + } + + /** + * @param $mapModels + * @return Model\MapModel[] + */ + public static function getFormattedMapData($mapModels){ + + $mapData = []; + foreach($mapModels as $mapModel){ + $mapData[] = [ + 'config' => $mapModel->getData(), 'data' => [ - 'systems' => $activeMap->getSystemData(), - 'connections' => $activeMap->getConnectionData(), + 'systems' => $mapModel->getSystemData(), + 'connections' => $mapModel->getConnectionData(), ] ]; } - - - - echo json_encode($newData); + return $mapData; } } \ No newline at end of file diff --git a/app/main/controller/api/System.php b/app/main/controller/api/System.php index 9dc780d9..57047c48 100644 --- a/app/main/controller/api/System.php +++ b/app/main/controller/api/System.php @@ -77,7 +77,7 @@ class System extends \Controller\AccessController { * build query * @return string */ - private function getQuery(){ + private function _getQuery(){ $query = $this->mainQuery; $query .= ' ' . $this->whereQuery; @@ -88,28 +88,24 @@ class System extends \Controller\AccessController { return $query; } + /** - * get system data by systemId - * @param $f3 - * @param $params + * get static system Data from CCPs Static DB export + * @param $systemId + * @return null */ - public function getById($f3, $params){ + protected function _getSystemModelById($systemId){ // switch DB $this->setDB('CCP'); - $systemId = ''; - // check for search parameter - if( array_key_exists( 'arg1', $params) ){ - $systemId = $params['arg1']; - } $this->whereQuery = "WHERE - map_sys.solarSystemID = " . $systemId . ""; + map_sys.solarSystemID = " . (int)$systemId . ""; $this->limitQuery = "Limit 1"; - $query = $this->getQuery(); + $query = $this->_getQuery(); $rows = $this->f3->get('DB')->exec($query, null, 30); @@ -120,15 +116,33 @@ class System extends \Controller\AccessController { // switch DB $this->setDB('PF'); - $system = Model\BasicModel::getNew('SystemModel'); $system->setData(reset($ccpData)); - $data = $system->getData(); - - echo json_encode($data); + return $system; } + /** + * Get all static system Data from CCP DB (long cache timer) + * @return array + */ + public function getSystems(){ + + // switch DB + $this->setDB('CCP'); + + $query = $this->_getQuery(); + + $rows = $this->f3->get('DB')->exec($query, null, 60 * 60 * 24); + + + // format result + $mapper = new Mapper\CcpSystemsMapper($rows); + + return $mapper->getData(); + } + + /** * search systems by name * @param $f3 @@ -148,9 +162,9 @@ class System extends \Controller\AccessController { $this->whereQuery = "WHERE map_sys.solarSystemName LIKE '%" . $searchToken . "%'"; - $query = $this->getQuery(); + $query = $this->_getQuery(); - $rows = $this->f3->get('DB')->exec($query); + $rows = $f3->get('DB')->exec($query); // format result $mapper = new Mapper\CcpSystemsMapper($rows); @@ -160,4 +174,130 @@ class System extends \Controller\AccessController { echo json_encode($data); } -} \ No newline at end of file + /** + * save a new system to a a map + * @param $f3 + */ + public function save($f3){ + + $newSystemData = []; + + $systemData = (array)$f3->get('POST.systemData'); + $mapData = (array)$f3->get('POST.mapData'); + + $map = Model\BasicModel::getNew('MapModel'); + $map->getById($mapData['id']); + + // check if map exists + if(!$map->dry()){ + + $systemData['mapId'] = $map; + + // get static system data (CCP DB) + $system = $this->_getSystemModelById($systemData['systemId']); + // set rest of system data + $system->setData($systemData); + $system->save(); + + $newSystemData = $system->getData(); + } + + echo json_encode($newSystemData); + } + + /** + * delete a system and all its connections + * @param $f3 + */ + public function delete($f3){ + $systemIds = $f3->get('POST.systemIds'); + + $user = $this->_getUser(); + $system = Model\BasicModel::getNew('SystemModel'); + + foreach($systemIds as $systemId){ + + $system->getById($systemId); + $system->delete($user); + + $system->reset(); + } + + echo json_encode([]); + } + + /** + * get system log data from CCP API import + * system Kills, Jumps,.... + * @param $f3 + */ + public function graphData($f3){ + $graphData = []; + $systemIds = $f3->get('POST.systemIds'); + + // number of log entries in each table per system (24 = 24h) + $logEntryCount = 24; + + // table names with system data + $logTables = [ + 'jumps' => 'SystemJumpModel', + 'shipKills' => 'SystemShipKillModel', + 'podKills' => 'SystemPodKillModel', + 'factionKills' => 'SystemFactionKillModel' + ]; + + foreach($systemIds as $systemId){ + + foreach($logTables as $label => $ModelClass){ + $systemLogModel = Model\BasicModel::getNew($ModelClass); + + // 10min cache (could be up to 1h cache time) + $systemLogModel->getByForeignKey('systemId', $systemId, array(), 60 * 10); + + if(!$systemLogModel->dry()){ + $counter = 0; + for( $i = $logEntryCount; $i >= 1; $i--){ + $column = 'value' . $i; + + // ship and pod kills should be merged into one table + if($label == 'podKills'){ + $graphData[$systemId]['shipKills'][$counter]['z'] = $systemLogModel->$column; + }else{ + $dataSet = [ + 'x' => ($i - 1) . 'h', + 'y' => $systemLogModel->$column + ]; + $graphData[$systemId][$label][] = $dataSet; + } + $counter++; + } + } + + } + } + + echo json_encode($graphData); + } + +} + + + + + + + + + + + + + + + + + + + + + diff --git a/app/main/controller/cron/Update.php b/app/main/controller/cron/Update.php new file mode 100644 index 00000000..22588375 --- /dev/null +++ b/app/main/controller/cron/Update.php @@ -0,0 +1,232 @@ + 5 + ]; + + /** + * table names for all system log tables + * @var array + */ + protected $logTables = [ + 'jumps' => 'system_jumps', + 'shipKills' => 'system_kills_ships', + 'podKills' => 'system_kills_pods', + 'factionKills' => 'system_kills_factions' + ]; + + /** + * event handler + */ + function beforeroute() { + + // controller access allowed for CLI-mode (command line) + if(php_sapi_name() != 'cli'){ + $this->f3->error(401, 'Cronjob: unauthorized access'); + } + + // set linebreak for status output + define('LNBR', PHP_EOL); + + parent::beforeroute(); + } + + /** + * check all system log tables for the correct number of system entries that will be locked + * @return array + */ + private function _prepareSystemLogTables(){ + + $systemController = new \Controller\Api\System(); + $systemsData = $systemController->getSystems(); + + // switch DB back to pathfinder + $this->setDB('PF'); + + // insert systems into each log table if not exist + $this->f3->get('DB')->begin(); + foreach($this->logTables as $tableName){ + + // insert systems into jump log table + $sqlInsertSystem = "INSERT IGNORE INTO " . $tableName . " (systemId) + VALUES(:systemId)"; + + foreach($systemsData as $systemData){ + // skip WH systems -> no jump data available + if($systemData['type']['name'] == 'k-space'){ + $this->f3->get('DB')->exec($sqlInsertSystem, array( + ':systemId' => $systemData['systemId'] + )); + } + } + + } + $this->f3->get('DB')->commit(); + + return $systemsData; + } + + + /** + * imports all relevant map stats from CCPs API + * >> php index.php "/cron/update/importmapdata" + * @param $f3 + */ + public function importMapData($f3){ + + $time_start = microtime(true); + // prepare system jump log table + $systemsData = $this->_prepareSystemLogTables(); + $time_end = microtime(true); + $execTimePrepareSystemLogTables = $time_end - $time_start; + + + // get current jump Data ------------------------------------------------------- + $time_start = microtime(true); + $apiPath = $this->pathEveApi . '/map/Jumps.xml.aspx'; + + $apiResponse = \Web::instance()->request($apiPath, $this->apiRequestOptions ); + + $jumpData = []; + $updateJumps = false; + if($apiResponse['body']){ + $xml = simplexml_load_string($apiResponse['body']); + $rowApiData = $xml->result->rowset; + + foreach($rowApiData->children() as $systemApiData){ + $attributeApiData = $systemApiData->attributes(); + $systemId = $attributeApiData->solarSystemID->__toString(); + $shipJumps =$attributeApiData->shipJumps->__toString(); + + $jumpData[$systemId] = $shipJumps; + } + + $updateJumps = true; + } + $time_end = microtime(true); + $execTimeGetJumpData = $time_end - $time_start; + + // get current kill Data ------------------------------------------------------- + $time_start = microtime(true); + $apiPath = $this->pathEveApi . '/map/Kills.xml.aspx'; + + $apiResponse = \Web::instance()->request($apiPath, $this->apiRequestOptions ); + $killData = []; + $updateKills = false; + if($apiResponse['body']){ + $xml = simplexml_load_string($apiResponse['body']); + $rowApiData = $xml->result->rowset; + foreach($rowApiData->children() as $systemApiData){ + $attributeApiData = $systemApiData->attributes(); + $systemId = $attributeApiData->solarSystemID->__toString(); + $shipKills =$attributeApiData->shipKills->__toString(); + $podKills =$attributeApiData->podKills->__toString(); + $factionKills =$attributeApiData->factionKills->__toString(); + + $killData[$systemId] = [ + 'shipKills' => $shipKills, + 'podKills' => $podKills, + 'factionKills' => $factionKills, + ]; + } + + $updateKills = true; + + } + $time_end = microtime(true); + $execTimeGetKillData = $time_end - $time_start; + + // update system log tables ----------------------------------------------------- + $time_start = microtime(true); + // make sure last update is (at least) 1h ago + $f3->get('DB')->begin(); + foreach($this->logTables as $key => $tableName){ + $sql = "UPDATE + " . $tableName . " + SET + value24 = value23, + value23 = value22, + value22 = value21, + value21 = value20, + value20 = value19, + value19 = value18, + value18 = value17, + value17 = value16, + value16 = value15, + value15 = value14, + value14 = value13, + value13 = value12, + value12 = value11, + value11 = value10, + value10 = value9, + value9 = value8, + value8 = value7, + value7 = value6, + value6 = value5, + value5 = value4, + value4 = value3, + value3 = value2, + value2 = value1, + value1 = :value + WHERE + systemId = :systemId AND + HOUR(TIMEDIFF(NOW(), updated)) > 0 + "; + + foreach($systemsData as $systemData){ + + if( + $key == 'jumps' && + $updateJumps + ){ + // update jump data (if available) + $currentJumps = 0; + if(array_key_exists($systemData['systemId'], $jumpData)){ + $currentJumps = $jumpData[$systemData['systemId']]; + } + + $f3->get('DB')->exec($sql, array( + ':systemId' => $systemData['systemId'], + ':value' => $currentJumps + )); + }else if($updateKills){ + + // update kill data (if available) + $currentKills = 0; + if(array_key_exists($systemData['systemId'], $killData)){ + $currentKillData = $killData[$systemData['systemId']]; + + $currentKills = $currentKillData[$key]; + } + + $f3->get('DB')->exec($sql, array( + ':systemId' => $systemData['systemId'], + ':value' => $currentKills + )); + } + } + } + $f3->get('DB')->commit(); + + $time_end = microtime(true); + $execTimeUpdateTables = $time_end - $time_start; + + // ------------------------ + echo 'prepare system log tables: ' . round($execTimePrepareSystemLogTables, 2) . 's' . LNBR; + echo 'get jump data: ' . round($execTimeGetJumpData, 2) . 's' . LNBR; + echo 'get kill data: ' . round($execTimeGetKillData, 2) . 's' . LNBR; + echo 'update log tables: ' . round($execTimeUpdateTables, 2) . 's' . LNBR; + } +} \ No newline at end of file diff --git a/app/main/model/BasicModel.php b/app/main/model/BasicModel.php index c7212ef9..9dff3778 100644 --- a/app/main/model/BasicModel.php +++ b/app/main/model/BasicModel.php @@ -14,6 +14,8 @@ use Exception; class BasicModel extends \DB\Cortex{ protected $db = 'DB'; + protected $ttl = 20; + protected $rel_ttl = 20; /** * field validation array @@ -82,7 +84,6 @@ class BasicModel extends \DB\Cortex{ } break; } - break; } } @@ -99,31 +100,46 @@ class BasicModel extends \DB\Cortex{ if(!$valid){ break; } - } } return $valid; } - /** * get single dataSet by id * @param $id - * @return array|FALSE + * @param int $ttl + * @return \DB\Cortex */ - public function getById($id) { - return $this->getByForeignKey('id', (int)$id, array('limit' => 1)); + public function getById($id, $ttl = 0) { + return $this->getByForeignKey('id', (int)$id, array('limit' => 1), $ttl); + } + + /** + * checks weather this model is active or not + * each model should have an "active" column + * @return bool + */ + public function isActive(){ + $isActive = false; + + if($this->active === 1){ + $isActive = true; + } + + return $isActive; } /** * get dataSet by foreign column * @param $key * @param $id - * @param $options - * @return array|FALSE + * @param array $options + * @param int $ttl + * @return \DB\Cortex */ - public function getByForeignKey($key, $id, $options = array()){ + public function getByForeignKey($key, $id, $options = array(), $ttl = 0){ $querySet = []; $query = []; @@ -140,34 +156,56 @@ class BasicModel extends \DB\Cortex{ array_unshift($querySet, implode(' AND ', $query)); - return $this->load( $querySet, $options ); + return $this->load( $querySet, $options, $ttl ); } /** * get multiple model obj that have an 1->m relation to this model * @param $model * @param $foreignKey + * @param null $options + * @param int $ttl * @return mixed */ - public function getRelatedModels($model, $foreignKey){ - $model = self::getNew($model); - $relatedModels = $model->find(array($foreignKey . ' = ? AND active = 1', $this->id), null, 10); + public function getRelatedModels($model, $foreignKey, $options = null, $ttl = 0){ + $model = self::getNew($model, $ttl); + $relatedModels = $model->find(array($foreignKey . ' = ? AND active = 1', $this->id), $options, $ttl); return $relatedModels; } + /** + * function should be overwritten in child classes with access restriction + * @param $accessObject + * @return bool + */ + public function hasAccess($accessObject){ + return true; + } + + /** + * function should be overwritten in parent classes + * @return bool + */ + public function isValid(){ + return true; + } + + /** * factory for all Models * @param $model + * @param int $ttl * @return null * @throws \Exception */ - public static function getNew($model){ + public static function getNew($model, $ttl = 20){ $class = null; $model = '\\' . __NAMESPACE__ . '\\' . $model; if(class_exists($model)){ - $class = new $model(); + $f3 = \Base::instance(); + $class = new $model($f3->get('DB'), null, null, $ttl ); }else{ throw new \Exception('No model class found'); } diff --git a/app/main/model/ConnectionModel.php b/app/main/model/ConnectionModel.php index 80d62f2e..daf06a45 100644 --- a/app/main/model/ConnectionModel.php +++ b/app/main/model/ConnectionModel.php @@ -12,6 +12,8 @@ namespace Model; class ConnectionModel extends BasicModel{ protected $table = 'connection'; + protected $ttl = 5; + protected $rel_ttl = 5; protected $fieldConf = array( 'mapId' => array( @@ -33,6 +35,13 @@ class ConnectionModel extends BasicModel{ if(!is_array($value)){ if($this->exists($key)){ $this->$key = $value; + + if($key == 'source'){ + // set mapId + $sourceSystem = self::getNew('SystemModel'); + $sourceSystem->getById( $this->$key ); + $this->mapId = $sourceSystem->mapId; + } } }elseif($key == 'type'){ // json field @@ -41,6 +50,10 @@ class ConnectionModel extends BasicModel{ } } + /** + * get connection data as array + * @return array + */ public function getData(){ $connectionData = [ @@ -55,4 +68,52 @@ class ConnectionModel extends BasicModel{ return $connectionData; } + /** + * check object for model access + * @param $accessObject + * @return bool + */ + public function hasAccess($accessObject){ + return $this->mapId->hasAccess($accessObject); + } + + /** + * check weather this model is valid or not + * @return bool + */ + public function isValid(){ + + $isValid = true; + // check if source/target belong to same map + $sourceSystem = self::getNew('SystemModel'); + $sourceSystem->getById( $this->source ); + + $targetSystem = self::getNew('SystemModel'); + $targetSystem->getById( $this->target); + + if( + $sourceSystem->dry() || + $targetSystem->dry() || + $sourceSystem->mapId->id !== $targetSystem->mapId->id + ){ + $isValid = false; + } + + return $isValid; + } + + /** + * delete a connection + * @param $accessObject + */ + public function delete($accessObject){ + + if(!$this->dry()){ + // check if editor has access + if($this->hasAccess($accessObject)){ + $this->erase(); + } + } + } + } \ No newline at end of file diff --git a/app/main/model/MapModel.php b/app/main/model/MapModel.php index 2ec3e416..f6fe7f6b 100644 --- a/app/main/model/MapModel.php +++ b/app/main/model/MapModel.php @@ -12,6 +12,8 @@ namespace Model; class MapModel extends BasicModel{ protected $table = 'map'; + protected $ttl = 5; + protected $rel_ttl = 5; protected $fieldConf = array( 'scopeId' => array( @@ -41,6 +43,10 @@ class MapModel extends BasicModel{ ], ]; + /** + * set map data by an associative array + * @param $data + */ public function setData($data){ foreach((array)$data as $key => $value){ @@ -77,7 +83,8 @@ class MapModel extends BasicModel{ ], 'type' => [ 'id' => $this->typeId->id, - 'name' => $this->typeId->name + 'name' => $this->typeId->name, + 'classTab' => $this->typeId->classTab ], 'icon' => $this->icon, 'updated' => strtotime($this->updated) @@ -93,11 +100,13 @@ class MapModel extends BasicModel{ * @return array */ public function getSystemData(){ - $systems = $this->getRelatedModels('SystemModel', 'mapId'); + $systems = $this->getRelatedModels('SystemModel', 'mapId', null, 5); $systemData = []; - foreach($systems as $system){ - $systemData[] = $system->getData(); + if(is_object($systems)){ + foreach($systems as $system){ + $systemData[] = $system->getData(); + } } return $systemData; @@ -108,16 +117,127 @@ class MapModel extends BasicModel{ * @return array */ public function getConnectionData(){ - $connections = $this->getRelatedModels('ConnectionModel', 'mapId'); + $connections = $this->getRelatedModels('ConnectionModel', 'mapId', null, 5); $connectionData = []; - foreach($connections as $connection){ - $connectionData[] = $connection->getData(); + if(is_object($connections)){ + foreach($connections as $connection){ + $connectionData[] = $connection->getData(); + } } return $connectionData; } + /** + * set map access for an object (user or alliance) + * @param $obj + */ + public function setAccess($obj){ + + if($obj instanceof UserModel){ + // private map + // get all userModels who have map access + $userMaps = $this->getRelatedModels('UserMapModel', 'mapId'); + + $userFound = false; + if($userMaps){ + foreach($userMaps as $userMap){ + if($userMap->userId->id !== $obj->id){ + // remove map access + $userMap->erase(); + }else{ + $userFound = true; + } + } + } + + if(!$userFound){ + // set user who has access to this map + $userMap = self::getNew('UserMapModel'); + $userMap->userId = $obj; + $userMap->mapId = $this; + $userMap->save(); + } + } + } + + /** + * checks weather an object (user or alliance) has + * @param $accessObject + * @return bool + */ + public function hasAccess($accessObject){ + $hasAccess = false; + + if($accessObject instanceof UserModel){ + // get all userModels who have map access + $userMaps = $this->getRelatedModels('UserMapModel', 'mapId'); + if($userMaps){ + foreach($userMaps as $userMap){ + if($userMap->userId->id === $accessObject->id){ + $hasAccess = true; + break; + } + } + } + } + + return $hasAccess; + } + + /** + * delete this map and all dependencies + */ + public function delete($accessObject){ + + if(!$this->dry()){ + // check if editor has access + if($this->hasAccess($accessObject)){ + // get all userModels who have map access + $userMaps = $this->getRelatedModels('UserMapModel', 'mapId'); + if(is_object($userMaps)){ + foreach($userMaps as $userMap){ + $userMap->erase(); + } + } + + // get all connections + $connections = $this->getRelatedModels('ConnectionModel', 'mapId'); + if(is_object($connections)){ + foreach($connections as $connection){ + $connection->erase(); + } + } + + // get all systems + $systems = $this->getRelatedModels('SystemModel', 'mapId'); + if(is_object($systems)){ + foreach($systems as $system){ + $system->erase(); + } + } + + // delete map + $this->erase(); + } + } + } + + /** + * checks weather a map is private or not + * @return bool + */ + public function isPrivate(){ + $isPrivate = false; + + if($this->typeId->id == 2){ + $isPrivate = true; + } + + return $isPrivate; + } + } \ No newline at end of file diff --git a/app/main/model/SystemFactionKillModel.php b/app/main/model/SystemFactionKillModel.php new file mode 100644 index 00000000..01829027 --- /dev/null +++ b/app/main/model/SystemFactionKillModel.php @@ -0,0 +1,15 @@ + array( @@ -24,7 +26,14 @@ class SystemModel extends BasicModel { 'belongs-to-one' => 'Model\SystemStatusModel' ) ); - +/* + protected $validate = [ + 'statusId' => [ + '' + 'regex' => '/^[1-9]+$/' + ] + ]; +*/ /** * set an array with all data for a system * @param $systemData @@ -60,7 +69,7 @@ class SystemModel extends BasicModel { } /** - * get map data for for response + * get map data as array * @return array */ public function getData(){ @@ -100,8 +109,66 @@ class SystemModel extends BasicModel { ]; - return $systemData; } + /** + * check object for model access + * @param $accessObject + * @return bool + */ + public function hasAccess($accessObject){ + return $this->mapId->hasAccess($accessObject); + } + + /** + * delete a system from a map + * @param $accessObject + */ + public function delete($accessObject){ + + if(!$this->dry()){ + // check if editor has access + if($this->hasAccess($accessObject)){ + // delete all system connections + $connections = $this->getConnections(); + + if(is_object($connections)){ + foreach($connections as $connection){ + $connection->erase(); + } + } + $this->erase(); + } + } + } + + /** + * get all connections for this system + * @return array + */ + public function getConnections(){ + $connections = false; + + // connections where system is source + $sourceConnections = $this->getRelatedModels('ConnectionModel', 'source'); + $targetConnections = $this->getRelatedModels('ConnectionModel', 'target'); + + if(is_object($sourceConnections)){ + $connections = $sourceConnections; + } + + if(is_object($targetConnections)){ + if(is_object($connections)){ + $connections->append($targetConnections); + }else{ + $connections = $targetConnections; + + } + } + + return $connections; + } + + } \ No newline at end of file diff --git a/app/main/model/SystemPodKillModel.php b/app/main/model/SystemPodKillModel.php new file mode 100644 index 00000000..be7e2b94 --- /dev/null +++ b/app/main/model/SystemPodKillModel.php @@ -0,0 +1,15 @@ + array( + 'belongs-to-one' => 'Model\UserModel' + ), 'mapId' => array( 'belongs-to-one' => 'Model\MapModel' ) diff --git a/app/main/model/UserModel.php b/app/main/model/UserModel.php index ba569def..1f56afcd 100644 --- a/app/main/model/UserModel.php +++ b/app/main/model/UserModel.php @@ -11,6 +11,8 @@ namespace Model; class UserModel extends BasicModel { protected $table = 'user'; + protected $ttl = 5; + protected $rel_ttl = 5; protected $validate = [ 'name' => [ @@ -68,11 +70,14 @@ class UserModel extends BasicModel { * @return array */ public function getMaps(){ - $userMaps = $this->getRelatedModels('UserMapModel', 'userId'); + + $userMaps = $this->getRelatedModels('UserMapModel', 'userId', null, 5); $maps = []; foreach($userMaps as $userMap){ - $maps[] = $userMap->mapId; + if($userMap->mapId->isActive()){ + $maps[] = $userMap->mapId; + } } return $maps; diff --git a/app/routes.cfg b/app/routes.cfg index c2bc0f83..6484edb3 100644 --- a/app/routes.cfg +++ b/app/routes.cfg @@ -2,8 +2,11 @@ ; program routes GET|POST /= Controller\MapController->showMap -GET|POST /login= Controller\Controller->showLogin +GET|POST /login= Controller\Controller->showLogin, 0, 512 -; api routes -GET|POST /api/@controller/@action = Controller\Api\@controller->@action -GET|POST /api/@controller/@action/@arg1 = Controller\Api\@controller->@action \ No newline at end of file +; APIs +GET|POST /api/@controller/@action = Controller\Api\@controller->@action, 0, 512 +GET|POST /api/@controller/@action/@arg1 = Controller\Api\@controller->@action, 0, 512 + +; cronjob APIs +GET /cron/@controller/@action = Controller\Cron\@controller->@action diff --git a/js/app.js b/js/app.js index 6bcad09e..60cde9ef 100644 --- a/js/app.js +++ b/js/app.js @@ -19,7 +19,7 @@ requirejs.config({ datatablesBootstrap: 'lib/datatables/dataTables.bootstrap', // DataTables - not used (bootstrap style) datatablesTableTools: 'lib/datatables/extensions/TableTools/js/dataTables.tableTools', // v2.2.3 TableTools (PlugIn) - https://datatables.net/extensions/tabletools/ xEditable: 'lib/bootstrap-editable.min', // v1.5.1 X-editable - in placed editing - morris: 'lib/morris.min', // v0.5.0 Morris.js - graphs and charts + morris: 'lib/morris.min', // v0.5.1 Morris.js - graphs and charts raphael: 'lib/raphael-min', // v2.1.2 Raphaƫl - required for morris (dependency) bootbox: 'lib/bootbox.min', // v4.3.0 Bootbox.js - custom dialogs easyPieChart: 'lib/jquery.easypiechart.min', // v2.1.6 Easy Pie Chart - HTML 5 pie charts - http://rendro.github.io/easy-pie-chart/ diff --git a/js/app/init.js b/js/app/init.js index 404056a8..72abb121 100644 --- a/js/app/init.js +++ b/js/app/init.js @@ -8,24 +8,31 @@ define(['jquery'], function($) { var Config = { timer: { + programStatusVisible: 5000, // ms: timer for status change visibility in head mapUpdate: { - delay: 3000, // delay between ping calls - executionLimit: 300 // log timelimit: main map update ping + delay: 3000, // ms: delay between ping calls + executionLimit: 200 // ms: log timelimit: main map update ping }, userUpdate: { - delay: 2000, // delay between ping calls - executionLimit: 100 // log timelimit: map user update ping + delay: 2000, // ms: delay between ping calls + executionLimit: 200 // ms: log timelimit: map user update ping }, mapModuleData: { - executionLimit: 100 // log timelimit: get all mapData + executionLimit: 100 // ms: log timelimit: get all mapData } }, path: { img: 'public/img/', // path for images - searchSystems: 'api/system/search', // ajax URL - search system by name - getSystem: 'api/system/getById', // ajax URL - get system by id initMap: 'api/map/init', // ajax URL - get static data - updateMapData: 'api/map/updateData' // ajax URL - main map update call + updateMapData: 'api/map/updateData', // ajax URL - main map update call + saveMap: 'api/map/save', // ajax URL - save/update map + deleteMap: 'api/map/delete', // ajax URL - delete map + searchSystem: 'api/system/search', // ajax URL - search system by name + saveSystem: 'api/system/save', // ajax URL - saves system to map + deleteSystem: 'api/system/delete', // ajax URL - delete system from map + saveConnection: 'api/connection/save', // ajax URL - save new connection to map + deleteConnection: 'api/connection/delete', // ajax URL - delete connection from map + getSystemGraphData: 'api/system/graphData' // ajax URL - get system graph data }, url: { zKillboard: 'https://zkillboard.com/api/', // killboard api @@ -33,7 +40,9 @@ define(['jquery'], function($) { }, animationSpeed: { headerLink: 100, // links in head bar - mapDeleteSystem: 200 // remove system from map + mapMoveSystem: 300, // system position has changed animation + mapDeleteSystem: 200, // remove system from map + mapModule: 100 // show/hide of an map module }, classes: { // log types diff --git a/js/app/logging.js b/js/app/logging.js index 4e0e7607..7c5a7e90 100644 --- a/js/app/logging.js +++ b/js/app/logging.js @@ -216,7 +216,7 @@ define([ lineColors: ['#375959'], pointFillColors: ['#477372'], pointStrokeColors: ['#313335'], - lineWidth: 3, + lineWidth: 2, grid: false, gridTextSize: 9, gridTextFamily: 'Oxygen Bold', diff --git a/js/app/main.js b/js/app/main.js index 6efdbe62..35911d36 100644 --- a/js/app/main.js +++ b/js/app/main.js @@ -11,6 +11,7 @@ define([ 'app/ccp', 'velocity', 'velocityUI', + 'app/ui/form_element', 'app/page', 'app/module_map' ], function($, Init, Util, Render, Logging, CCP) { @@ -32,6 +33,9 @@ define([ $('body').loadPageStructure(); // Map init options + var mapData = []; + + /* var mapData =[{ map: {}, config: { @@ -520,7 +524,7 @@ define([ connections: [] } }]; - +*/ // current user Data for a map var tempUserData ={ @@ -587,16 +591,16 @@ define([ } },{ config: { // map config - id: 2 // map id + id: 128 // map id }, data: { systems:[ // systems in map { - id: 50, // system id + id: 8597, // system id user: [ { id: 6, - name: 'Schleiferius', + name: 'Exodus 6D Gidrine', ship: { id: 69, name: 'Tengu' @@ -624,7 +628,6 @@ define([ Init.systemStatus = initData.systemStatus; Init.systemType = initData.systemType; - console.log(Init.systemType); // init map module mapModule.initMapModule(); @@ -636,10 +639,6 @@ define([ }); - - - - $.fn.initMapModule = function(){ var mapModule = $(this); @@ -653,7 +652,7 @@ define([ var mapUserUpdateDelay = Init.timer[mapUserUpdateKey].delay; // ping for main map update - var triggerMapUpdatePing = function(tempMapData){ + var triggerMapUpdatePing = function(){ // check each execution time if map module is still available var check = $('#' + mapModule.attr('id')).length; @@ -663,16 +662,6 @@ define([ return; } - $(document).setProgramStatus('online'); - - Util.timeStart(mapUpdateKey); - // load map module ========================================== - mapModule.updateMapModule(tempMapData); - var duration = Util.timeStop(mapUpdateKey); - - // log execution time - Util.log(mapUpdateKey, {duration: duration, description: 'updateMapModule'}); - // get updated map data Util.timeStart(mapModuleDatakey); var updatedMapData = mapModule.getMapModuleData(); @@ -684,30 +673,47 @@ define([ // wrap array to object updatedMapData = {mapData: updatedMapData}; + // start log + Util.timeStart(mapUpdateKey); + // store updatedMapData $.ajax({ type: 'POST', url: Init.path.updateMapData, data: updatedMapData, dataType: 'json' - }).done(function(data){ + }).done(function(mapData){ - mapData = data; + $(document).setProgramStatus('online'); + if(mapData.length === 0){ + // no map data available -> show "new map" dialog + $(document).trigger('pf:menuEditMap', {newMap: true}); + }else{ + // map data found + + // load map module + mapModule.updateMapModule(mapData); + + // log execution time + var duration = Util.timeStop(mapUpdateKey); + Util.log(mapUpdateKey, {duration: duration, description: 'updateMapModule'}); + } // init new trigger setTimeout(function(){ - triggerMapUpdatePing(mapData); + triggerMapUpdatePing(); }, mapUpdateDelay); }).fail(function( jqXHR, status, error) { - var reason = status + ': ' + error; - console.log(error); - Util.showNotify({title: jqXHR.status + ': updatedMapData', text: reason, type: 'warning'}); - $(document).setProgramStatus('problem'); + var reason = status + ' ' + jqXHR.status + ': ' + error; + + Util.emergencyShutdown(reason); }); + + }; - triggerMapUpdatePing(mapData); + triggerMapUpdatePing([]); // ping for user data update ------------------------------------------------------- var triggerUserUpdatePing = function(userData){ @@ -715,7 +721,7 @@ define([ $(document).setProgramStatus('online'); Util.timeStart(mapUserUpdateKey); - mapModule.updateMapModuleData(userData); + // mapModule.updateMapModuleData(userData); var duration = Util.timeStop(mapUserUpdateKey); // log execution time diff --git a/js/app/map/map.js b/js/app/map/map.js index 84f06d4e..e295a678 100644 --- a/js/app/map/map.js +++ b/js/app/map/map.js @@ -61,7 +61,7 @@ define([ // dialogs systemDialogId: 'pf-system-dialog', // id for system dialog - systemDialogSelectClass: 'pf-system-dialog-select', // id for system select Element + systemDialogSelectClass: 'pf-system-dialog-select', // class for system select Element // system security classes systemSec: 'pf-system-sec', @@ -108,8 +108,6 @@ define([ console.log(info.maxConnections); }, beforeDetach:function(connection) { - var mapElement = connection._jsPlumb.instance.getContainer(); - $(mapElement).getMapOverlay().startMapUpdateCounter(); return true; }/*, @@ -503,6 +501,9 @@ define([ // check if system already exists var system = document.getElementById( systemId ); + var newPosX = data.position.x + 'px'; + var newPosY = data.position.y + 'px'; + if(!system){ // get system info classes @@ -552,16 +553,43 @@ define([ }) ).data('name', data.name); + // set initial system position + system.css({ + 'left': newPosX, + 'top': newPosY + }); }else{ system = $(system); + + // set system position + var currentPosX = system.css('left'); + var currentPosY = system.css('top'); + + if( + newPosX !== currentPosX || + newPosY !== currentPosY + ){ + + // change position with animation + system.velocity( + { + left: newPosX, + top: newPosY + },{ + easing: 'linear', + duration: Init.animationSpeed.mapMoveSystem, + progress: function(){ + map.revalidate( systemId ); + }, + complete: function(){ + map.revalidate( systemId ); + } + } + ); + } } - // set system position - system.css({ - 'left': data.position.x + 'px', - 'top': data.position.y + 'px' } - ); // set system name or alias var systemName = data.name; @@ -581,7 +609,7 @@ define([ system.data('id', parseInt(data.id)); system.data('systemId', parseInt(data.systemId)); system.data('name', data.name); - system.data('typeId', data.type.id); + system.data('typeId', parseInt(data.type.id)); system.data('effect', data.effect); system.data('security', data.security); system.data('trueSec', parseFloat(data.trueSec)); @@ -589,9 +617,8 @@ define([ system.data('region', data.region.name); system.data('constellationId', parseInt(data.constellation.id)); system.data('constellation', data.constellation.name); - - system.data('updated', data.updated); - system.attr('data-mapid', mapContainer.data('id')); + system.data('updated', parseInt(data.updated)); + system.attr('data-mapid', parseInt(mapContainer.data('id'))); // locked system if( Boolean( system.data( 'locked') ) !== Boolean( parseInt( data.locked ) )){ @@ -650,12 +677,15 @@ define([ mapContainer = $(mapContainer); - // add additional information - mapContainer.data('name', mapConfig.config.name); - mapContainer.data('scopeId', mapConfig.config.scope.id); - mapContainer.data('typeId', mapConfig.config.type.id); - mapContainer.data('icon', mapConfig.config.icon); - mapContainer.data('updated', mapConfig.config.updated); + // add additional information for this map + if(mapContainer.data('updated') !== mapConfig.config.updated){ + mapContainer.data('name', mapConfig.config.name); + mapContainer.data('scopeId', mapConfig.config.scope.id); + mapContainer.data('typeId', mapConfig.config.type.id); + mapContainer.data('icon', mapConfig.config.icon); + mapContainer.data('updated', mapConfig.config.updated); + } + // get map data var mapData = mapContainer.getMapData(); @@ -675,7 +705,6 @@ define([ for(var k = 0; k < currentSystemData.length; k++){ if(currentSystemData[k].id === systemData.id){ - if( currentSystemData[k].updated < systemData.updated ){ // system changed -> update mapContainer.getSystem(mapConfig.map, systemData); @@ -706,8 +735,10 @@ define([ } if(deleteThisSystem === true){ + var deleteSystem = $('#' + config.systemIdPrefix + mapContainer.data('id') + '-' + currentSystemData[a].id); + // system not found -> delete system - deleteSystem(mapConfig.map, $('#' + config.systemIdPrefix + currentSystemData[a].id)); + removeSystem(mapConfig.map, deleteSystem); } } @@ -721,16 +752,16 @@ define([ var addNewConnection= true; for(var c = 0; c < currentConnectionData.length; c++){ - if(currentConnectionData[c].id === connectionData.id){ + if( + currentConnectionData[c].id === connectionData.id + ){ // connection already exists -> check for updates - if( - currentConnectionData[c].updated < connectionData.updated && // has changed - ativeConnections[mapData.config.id][connectionData.id] !== undefined + currentConnectionData[c].updated < connectionData.updated ){ // connection changed -> update var tempConnection = ativeConnections[mapData.config.id][connectionData.id]; - updateConnection(tempConnection, connectionData, currentConnectionData[c]); + updateConnection(tempConnection, currentConnectionData[c], connectionData); } addNewConnection = false; @@ -756,14 +787,14 @@ define([ break; } } - deleteThisConnection = true; + if( deleteThisConnection === true && - ativeConnections[mapData.config.id][currentConnectionData.id] !== undefined + ativeConnections[mapData.config.id][currentConnectionData[d].id] !== undefined ){ // connection not found -> delete connection - var deleteConnection = ativeConnections[mapData.config.id][currentConnectionData.id]; - mapConfig.map.detach(deleteConnection); + var deleteConnection = ativeConnections[mapData.config.id][currentConnectionData[d].id]; + mapConfig.map.detach(deleteConnection, {fireEvent: false}); } } @@ -821,13 +852,13 @@ define([ endpointElements.velocity('transition.fadeIn', { stagger: 50, drag: true, - duration: 50 + duration: 50, }); connectorElements.velocity('transition.flipBounceXIn', { stagger: 50, drag: true, - duration: 1000 + duration: 1000, }); overlayElements.delay(500).velocity('transition.fadeIn', { @@ -848,14 +879,14 @@ define([ stagger: 0, drag: false, duration: 200, - display: 'auto' + display: 'auto', }); overlayElements.velocity('transition.fadeOut', { stagger: 50, drag: true, duration: 200, - display: 'auto' + display: 'auto', }); endpointElements.velocity('transition.fadeOut', { @@ -869,7 +900,7 @@ define([ stagger: 0, drag: true, duration: 20, - display: 'block' + display: 'block', }); systemElements.delay(100).velocity('transition.slideUpOut', { @@ -941,7 +972,7 @@ define([ * @param systemData * @param {String[]} optional Systems for connection */ - var drawSystem = function(map, systemData, connectedSystems){ + var drawSystem = function(map, systemData, connectedSystem){ // check if systemData is valid if(isValidSystem(systemData)){ @@ -966,28 +997,66 @@ define([ setSystemObserver(map, newSystem); // connect new system (if connection data is given) - if(connectedSystems){ + if(connectedSystem){ - $.each(connectedSystems, function(i, connectSystem){ + var connectionData = { + source: $(connectedSystem).data('id'), + target: newSystem.data('id'), + type: ['wh'] + }; + var connection = drawConnection(map, connectionData); - var connectionData = { - source: $(connectSystem).data('id'), - target: $(newSystem).data('id'), - type: ['wh'] - }; - drawConnection(map, connectionData); - }); + // store connection + saveConnection(connection); } } }; /** * delete a system with all its connections + * (ajax call) remove system from DB + * @param map + * @param systems + * @param callback function + */ + var deleteSystems = function(map, systems, callback){ + + var systemIds = []; + // systemIds for delete request + for(var i = 0; i < systems.length; i++){ + systemIds.push( $(systems[i]).data('id') ); + } + + var requestData = { + systemIds: systemIds + }; + + $.ajax({ + type: 'POST', + url: Init.path.deleteSystem, + data: requestData, + dataType: 'json' + }).done(function(data){ + + // remove systems from map + for(var i = 0; i < systems.length; i++){ + removeSystem(map, $(systems[i])); + } + + callback(); + }).fail(function( jqXHR, status, error) { + var reason = status + ' ' + error; + Util.showNotify({title: jqXHR.status + ': deleteSystem', text: reason, type: 'warning'}); + $(document).setProgramStatus('problem'); + }); + }; + + /** + * remove a system from map (no backend requests) * @param map * @param system */ - var deleteSystem = function(map, system){ - + var removeSystem = function(map, system){ system = $(system); // detach all connections @@ -1006,8 +1075,10 @@ define([ $(this).remove() ; } }); + }; + /** * make a system name editable by x-editable * @param system @@ -1026,7 +1097,6 @@ define([ showbuttons: false }); - // update z-index for system, editable field should be on top $(headElement).on('shown', function(e, editable) { updateZIndex(system); @@ -1150,7 +1220,7 @@ define([ }; /** - * update z-index for a system (dragged sytems should be always on top) + * update z-index for a system (dragged systems should be always on top) * @param system */ var updateZIndex = function(system){ @@ -1223,7 +1293,7 @@ define([ // connection have the default map Scope scope var scope = map.Defaults.Scope; if(connectionData.scope){ - scope = connectionData.scope.name; + scope = connectionData.scope; } var connection = map.connect({ @@ -1256,6 +1326,91 @@ define([ return connection; }; + /** + * stores a connection in database + * @param connection + */ + var saveConnection = function(connection){ + + var connectionData = getDataByConnection(connection); + + var requestData = { + connectionData: connectionData + }; + + $.ajax({ + type: 'POST', + url: Init.path.saveConnection, + data: requestData, + dataType: 'json' + }).done(function(newConnectionData){ + + // update connection data + connection.setParameter('connectionId', newConnectionData.id); + connection.setParameter('updated', newConnectionData.updated); + + var text = 'New connection established'; + if(connectionData.id > 0){ + text = 'Connection switched'; + } + + Util.showNotify({title: text, type: 'success'}); + }).fail(function( jqXHR, status, error) { + + // remove this connection from map + connection._jsPlumb.instance.detach(connection); + + var reason = status + ' ' + error; + Util.showNotify({title: jqXHR.status + ': saveConnection', text: reason, type: 'warning'}); + $(document).setProgramStatus('problem'); + }); + }; + + /** + * Programmatically delete a connection and all related data + * @param connections + */ + var deleteConnections = function(connections){ + + var connectionIds = []; + // systemIds for delete request + for(var i = 0; i < connections.length; i++){ + var connectionId = connections[i].getParameter('connectionId'); + // drag&drop a new connection does not have an id yet, if connection is not established correct + if(connectionId !== undefined){ + connectionIds[i] = connections[i].getParameter('connectionId'); + } + } + + if(connectionIds.length > 0){ + var requestData = { + connectionIds: connectionIds + }; + + $.ajax({ + type: 'POST', + url: Init.path.deleteConnection, + data: requestData, + dataType: 'json' + }).done(function(data){ + + // remove systems from map + for(var i = 0; i < connections.length; i++){ + // if a connection is manually (drag&drop) detached, the jsPlumb instance does not exist any more + // connection is already deleted! + if(connections[i]._jsPlumb){ + connections[i]._jsPlumb.instance.detach(connections[i], {fireEvent: false}); + } + } + }).fail(function( jqXHR, status, error) { + var reason = status + ' ' + error; + Util.showNotify({title: jqXHR.status + ': deleteSystem', text: reason, type: 'warning'}); + $(document).setProgramStatus('problem'); + }); + } + + }; + /** * compares the current data and new data of a connection and updates status * @param connection @@ -1265,6 +1420,8 @@ define([ var updateConnection = function(connection, connectionData, newConnectionData){ var map = connection._jsPlumb.instance; + var mapContainer = $( map.getContainer() ); + var mapId = mapContainer.data('id'); // check scope if(connectionData.scope !== newConnectionData.scope){ @@ -1274,6 +1431,15 @@ define([ var addType = $(newConnectionData.type).not(connectionData.type).get(); var removeType = $(connectionData.type).not(newConnectionData.type).get(); + // check if source or target has changed + if(connectionData.source !== newConnectionData.source ){ + map.setSource(connection, config.systemIdPrefix + mapId + '-' + newConnectionData.source); + } + if(connectionData.target !== newConnectionData.target ){ + map.setTarget(connection, config.systemIdPrefix + mapId + '-' + newConnectionData.target); + } + + // connection.targetId // add types for(var i = 0; i < addType.length; i++){ if( @@ -1300,6 +1466,11 @@ define([ setConnectionObserver(map, connection); } } + + // set update date + connection.setParameter('updated', newConnectionData.updated); + + }; /** @@ -1521,7 +1692,6 @@ define([ $(selectedSystems).toggleSystemTooltip('hide', {}); }, drag: function(){ - }, stop: function(params){ var dragSystem = $(params.el); @@ -1541,9 +1711,15 @@ define([ selectedSystems = selectedSystems.concat( dragSystem.get() ); selectedSystems = $.unique( selectedSystems ); - // set new position for popover edit field (system name) + for(var i = 0; i < selectedSystems.length; i++){ var tempSystem = $(selectedSystems[i]); + + // set all selected systems as "changes" for update + tempSystem.markAsChanged(); + + + // set new position for popover edit field (system name) var tempPosition = tempSystem.position(); var placement = 'top'; @@ -1672,17 +1848,16 @@ define([ break; case 'lock_system': // lock system - currentSystem.toggleLockSystem(true, {map: map}); // repaint connections, -> system changed its size! map.repaint( currentSystem ); + + currentSystem.markAsChanged(); break; case 'set_rally': // set rally point - if( ! currentSystem.data( 'rally' ) ){ - // show confirm dialog var rallyDialog = bootbox.dialog({ message: 'Do you want to poke active pilots?', @@ -1700,6 +1875,7 @@ define([ className: 'btn-info', callback: function() { currentSystem.toggleRallyPoint(true, {}); + currentSystem.markAsChanged(); } }, setRallay: { @@ -1707,6 +1883,7 @@ define([ className: 'btn-primary', callback: function() { currentSystem.toggleRallyPoint(false, {}); + currentSystem.markAsChanged(); } } } @@ -1714,6 +1891,7 @@ define([ }else{ // remove rally point currentSystem.toggleRallyPoint(false, {}); + currentSystem.markAsChanged(); } break; case 'change_status_unknown': @@ -1728,24 +1906,26 @@ define([ var statusString = action.split('_'); currentSystem.setSystemStatus(statusString[2]); + + currentSystem.markAsChanged(); break; case 'delete_system': // confirm dialog - bootbox.confirm('Delete system and all its connections?', function(result) { + var systemDeleteDialog = bootbox.confirm('Delete system and all its connections?', function(result) { if(result){ - $(currentSystem).getMapOverlay().startMapUpdateCounter(); - - var systemName = currentSystem.getSystemInfo(['alias']); - deleteSystem(map, currentSystem); + deleteSystems(map, [currentSystem], function(){ + // callback function after delete -> close dialog - Util.showNotify({title: 'System deleted', text: systemName, type: 'success'}); + $(systemDeleteDialog).modal('hide'); + Util.showNotify({title: 'System deleted', text: systemName, type: 'success'}); + }); + + return false; } }); - break; } - } }); @@ -1755,7 +1935,6 @@ define([ // left mouse button if(e.which === 1){ - if(! system.hasClass('no-click')){ if(e.ctrlKey === true){ // select system @@ -1768,6 +1947,29 @@ define([ }); }; + /** + * mark a dom element (map, system, connection) as changed + * DB will update this element on next update trigger + */ + $.fn.markAsChanged = function(){ + return this.each(function(){ + var element = $(this); + + if(element.hasClass(config.systemClass)){ + // system element + element.data('updated', 0); + }else{ + // connection element + this.setParameter('updated', 0); + } + + }); + }; + + /** + * triggers the "showSystemInfo" event, that is responsible for initializing the "map info" panel + * @param map + */ $.fn.showSystemInfo = function(map){ var system = $(this); @@ -2048,15 +2250,16 @@ define([ var selectedSystems = $(currentMapElement).getSelectedSystems(); if(selectedSystems.length > 0){ - bootbox.confirm('Delete ' + selectedSystems.length + ' selected systems and its connections?', function(result) { + var systemDeleteDialog = bootbox.confirm('Delete ' + selectedSystems.length + ' selected systems and its connections?', function(result) { if(result){ currentMapElement.getMapOverlay().startMapUpdateCounter(); - for(var i = 0; i < selectedSystems.length; i++){ - deleteSystem(currentMap, selectedSystems[i]); - } + deleteSystems(currentMap, selectedSystems, function(){ + // callback function after delete -> close dialog - Util.showNotify({title: selectedSystems.length + ' systems deleted', type: 'success'}); + $(systemDeleteDialog).modal('hide'); + Util.showNotify({title: selectedSystems.length + ' systems deleted', type: 'success'}); + }); } }); }else{ @@ -2288,9 +2491,7 @@ define([ // confirm dialog bootbox.confirm('Is this connection really gone?', function(result) { if(result){ - mapElement.getMapOverlay().startMapUpdateCounter(); - - map.detach(params.component); + deleteConnections([params.component]); } }); break; @@ -2303,6 +2504,7 @@ define([ activeConnection.toggleType( action ); // for some reason a new observer is needed ?! setConnectionObserver(map, activeConnection); + $(activeConnection).markAsChanged(); break; case 'status_fresh': case 'status_reduced': @@ -2311,6 +2513,7 @@ define([ mapElement.getMapOverlay().startMapUpdateCounter(); setConnectionWHStatus(activeConnection, 'wh_' + newStatus); + $(activeConnection).markAsChanged(); break; case 'scope_wh': case 'scope_stargate': @@ -2329,6 +2532,8 @@ define([ var scopeLabel = Util.getScopeInfoForMap(newScope, 'label'); Util.showNotify({title: 'Connection scope changed', text: 'New scope: ' + scopeLabel, type: 'success'}); + + $(activeConnection).markAsChanged(); } }); @@ -2401,17 +2606,16 @@ define([ // format system status for form select var systemStatus = {}; - $.each(Init.systemStatus, function(status, statusData){ - //statusData.status = status; - //systemStatus.push(statusData); - systemStatus[status] = statusData.label; + systemStatus[statusData.id] = statusData.label; }); + // default system status -> first status entry + var defaultSystemStatus = Init.systemStatus[Object.keys(Init.systemStatus)[0]].id; + var data = { id: config.systemDialogId, - status: systemStatus, selectClass: config.systemDialogSelectClass }; @@ -2451,64 +2655,75 @@ define([ if(formValid === false){ // don't close dialog return false; - } - // get system information - var requestUrl = Init.path.getSystem + '/' + parseInt( systemDialogData.systemId ); - $.getJSON( requestUrl, function( systemData ) { + // calculate new system position ----------------------------------------------- + var currentX = 0; + var currentY = 0; - mapContainer.getMapOverlay().startMapUpdateCounter(); + var newPositon = { + x: 0, + y: 0 + }; - // merge dialog data and backend data - var newSystemData = $.extend(systemData, systemDialogData); + var sourceSystem = null; - var currentX = 0; - var currentY = 0; + // add new position + if(options.sourceSystem !== undefined){ - var newPositon = { - x: 0, - y: 0 + sourceSystem = options.sourceSystem; + + // related system is available + currentX = sourceSystem.css('left'); + currentY = sourceSystem.css('top'); + + // remove "px" + currentX = parseInt( currentX.substring(0, currentX.length - 2) ); + currentY = parseInt( currentY.substring(0, currentY.length - 2) ); + + newPositon = { + x: currentX + config.newSystemOffset.x, + y: currentY + config.newSystemOffset.y }; + }else{ + // check mouse cursor position (add system to map) + newPositon = { + x: options.position.x, + y: options.position.y + }; + } - var sourceSystem = null; + systemDialogData.position = newPositon; - // add new position - if(options.sourceSystem !== undefined){ + // ----------------------------------------------------------------------------- - sourceSystem = options.sourceSystem; - - // related system is available - currentX = sourceSystem.css('left'); - currentY = sourceSystem.css('top'); - - // remove "px" - currentX = parseInt( currentX.substring(0, currentX.length - 2) ); - currentY = parseInt( currentY.substring(0, currentY.length - 2) ); - - newPositon = { - x: currentX + config.newSystemOffset.x, - y: currentY + config.newSystemOffset.y - }; - }else{ - // check mouse cursor position (add system to map) - newPositon = { - x: options.position.x, - y: options.position.y - }; + var requestData = { + systemData: systemDialogData, + mapData: { + id: mapContainer.data('id') } + }; - newSystemData.position = newPositon; + $.ajax({ + type: 'POST', + url: Init.path.saveSystem, + data: requestData, + dataType: 'json' + }).done(function(newSystemData){ // draw new system to map drawSystem(map, newSystemData, sourceSystem); + Util.showNotify({title: 'New system', text: newSystemData.name, type: 'success'}); + $(systemDialog).modal('hide'); }).fail(function( jqXHR, status, error) { - var reason = status + ': ' + error; - Util.showNotify({title: jqXHR.status + ': System search failed', text: reason, type: 'warning'}); + var reason = status + ' ' + error; + Util.showNotify({title: jqXHR.status + ': saveSystem', text: reason, type: 'warning'}); + $(document).setProgramStatus('problem'); }); + return false; } } } @@ -2521,59 +2736,10 @@ define([ // init system select live search var selectElement = modalContent.find('.' + config.systemDialogSelectClass); - - $.when( - selectElement.select2({ - ajax: { - url: function(params){ - // add params to URL - return Init.path.searchSystems + '/' + params.term; - }, - dataType: 'json', - delay: 250, - data: function(params) { - // no url params here - return; - }, - processResults: function(data) { - // parse the results into the format expected by Select2. - return { - results: data.map( function(item){ - - var secClass = Util.getSecurityClassForSystem(item.security); - - var systemSecurity = ' '; - systemSecurity += '(' + item.security + ')'; - - return { - id: item.systemId, - text: item.name + systemSecurity - }; - }) - }; - }, - error: function (jqXHR, status, error) { - // close select - selectElement.select2('destroy'); - - var reason = status + ' ' + jqXHR.status + ': ' + error; - Util.emergencyShutdown(reason); - } - }, - minimumInputLength: 2, - placeholder: 'Jita', - allowClear: true - }) - ).done(function(){ - // open select - selectElement.select2('open'); - }); - + selectElement.initSystemSelect({key: 'systemId'}); }); - - - // make dialog editable + // init system status select var modalFields = $('.bootbox .modal-dialog').find('.pf-editable-system-status'); modalFields.editable({ @@ -2582,6 +2748,7 @@ define([ onblur: 'submit', showbuttons: false, source: systemStatus, + value: defaultSystemStatus, inputclass: config.systemDialogSelectClass }); @@ -2710,54 +2877,13 @@ define([ // map data ----------------------------------- var data = {}; + // systems data ------------------------------------ var systemsData = []; - var systems = mapElement.find('.' + config.systemClass); for(var i = 0; i < systems.length; i++){ - - // systems data ------------------------------------ var tempSystem = $(systems[i]); - var systemData = {}; - systemData.id = parseInt( tempSystem.data('id') ); - systemData.systemId = parseInt( tempSystem.data('systemId') ); - systemData.name = tempSystem.data('name'); - systemData.alias = tempSystem.getSystemInfo(['alias']); - systemData.effect = tempSystem.data('effect'); - systemData.type = { - id: tempSystem.data('typeId') - }; - systemData.security = tempSystem.data('security'); - systemData.trueSec = tempSystem.data('trueSec'); - systemData.region = { - id: tempSystem.data('regionId'), - name: tempSystem.data('region') - }; - systemData.constellation = { - id: tempSystem.data('constellationId'), - name: tempSystem.data('constellation') - }; - systemData.status = { - id: tempSystem.data('statusId') - }; - systemData.locked = tempSystem.data('locked') ? 1 : 0; - systemData.rally = tempSystem.data('rally') ? 1 : 0; - systemData.currentUser = tempSystem.data('currentUser'); - systemData.updated = parseInt( tempSystem.data('updated') ); - systemData.userCount = (tempSystem.data('userCount') ? parseInt( tempSystem.data('userCount') ) : 0); - - // position ------------------------------- - var positionData = {}; - var currentX = tempSystem.css('left'); - var currentY = tempSystem.css('top'); - - // remove 'px' - positionData.x = parseInt( currentX.substring(0, currentX.length - 2) ); - positionData.y = parseInt( currentY.substring(0, currentY.length - 2) ); - - systemData.position = positionData; - - systemsData.push(systemData); + systemsData.push( tempSystem.getSystemData() ); } data.systems = systemsData; @@ -2773,28 +2899,12 @@ define([ for(var j = 0; j < connections.length; j++){ var tempConnection = connections[j]; - var source = $(tempConnection.source); - var target = $(tempConnection.target); - - var connectionId = tempConnection.getParameter('connectionId'); - var updated = tempConnection.getParameter('updated'); - - - var connection = { - id: connectionId, - source: source.data('id'), - sourceName: source.data('name'), - target: target.data('id'), - targetName: target.data('name'), - scope: tempConnection.scope, - type: tempConnection.getType(), - updated: updated - }; + var connectionData = getDataByConnection(tempConnection); // add to cache - ativeConnections[mapConfig.id][connectionId] = tempConnection; + ativeConnections[mapConfig.id][connectionData.id] = tempConnection; - connectionsFormatted.push(connection); + connectionsFormatted.push( connectionData ); } data.connections = connectionsFormatted; @@ -2807,6 +2917,82 @@ define([ return mapData; }; + /** + * get all relevant data for a system object + * @returns {{}} + */ + $.fn.getSystemData = function(){ + var system = $(this); + + var systemData = {}; + systemData.id = parseInt( system.data('id') ); + systemData.systemId = parseInt( system.data('systemId') ); + systemData.name = system.data('name'); + systemData.alias = system.getSystemInfo(['alias']); + systemData.effect = system.data('effect'); + systemData.type = { + id: system.data('typeId') + }; + systemData.security = system.data('security'); + systemData.trueSec = system.data('trueSec'); + systemData.region = { + id: system.data('regionId'), + name: system.data('region') + }; + systemData.constellation = { + id: system.data('constellationId'), + name: system.data('constellation') + }; + systemData.status = { + id: system.data('statusId') + }; + systemData.locked = system.data('locked') ? 1 : 0; + systemData.rally = system.data('rally') ? 1 : 0; + systemData.currentUser = system.data('currentUser'); + systemData.updated = parseInt( system.data('updated') ); + systemData.userCount = (system.data('userCount') ? parseInt( system.data('userCount') ) : 0); + + // position ------------------------------- + var positionData = {}; + var currentX = system.css('left'); + var currentY = system.css('top'); + + // remove 'px' + positionData.x = parseInt( currentX.substring(0, currentX.length - 2) ); + positionData.y = parseInt( currentY.substring(0, currentY.length - 2) ); + + systemData.position = positionData; + + return systemData; + }; + + /** + * get all relevant data for a connection object + * @param connection + * @returns {{id: Number, source: Number, sourceName: (*|T|JQuery|{}), target: Number, targetName: (*|T|JQuery), scope: *, type: *, updated: Number}} + */ + var getDataByConnection = function(connection){ + + var source = $(connection.source); + var target = $(connection.target); + + var id = connection.getParameter('connectionId'); + var updated = connection.getParameter('updated'); + + var data = { + id: id ? id : 0, + source: parseInt( source.data('id') ), + sourceName: source.data('name'), + target: parseInt( target.data('id') ), + targetName: target.data('name'), + scope: connection.scope, + type: connection.getType(), + updated: updated ? updated : 0 + }; + + return data; + }; + var getMapInstance = function(mapId){ @@ -2838,57 +3024,47 @@ define([ setConnectionObserver(newJsPlumbInstance, info.connection); }); - newJsPlumbInstance.bind('connectionDetached', function(info, e) { + newJsPlumbInstance.bind('connectionDetached', function(info, e){ + // a connection is manually (drag&drop) detached! otherwise this event should not be send! + var connection = info.connection; + deleteConnections([connection]); }); // event after DragStop a connection or new connection newJsPlumbInstance.bind('beforeDrop', function(info) { var connection = info.connection; - var sourceSystem = $('#' + info.sourceId); - var returnValue = true; - - var connectionId = connection.getParameter('connectionId'); - - sourceSystem.getMapOverlay().startMapUpdateCounter(); // set "default" connection status only for NEW connections if(!connection.suspendedElement){ setConnectionWHStatus(connection, 'wh_fresh'); } - // prevent multiple connections between same systems + var sourceSystem = $('#' + info.sourceId); + + // prevent multiple connections between same systems ---------------------------- var connections = checkForConnection(newJsPlumbInstance, info.sourceId, info.targetId ); if(connections.length > 1){ bootbox.confirm('Connection already exists. Do you really want to add an additional one?', function(result) { if(!result){ - newJsPlumbInstance.detach(connection); - sourceSystem.getMapOverlay().startMapUpdateCounter(); + connection._jsPlumb.instance.detach(connection); } }); + }else{ + // save new connection + saveConnection(connection); } + // ----------------------------------------------------------------------------- - // notification - if(returnValue === true){ + return true; - var text = 'New connection established'; - if(connectionId > 0){ - text = 'connection switched'; - } - - Util.showNotify({title: text, type: 'success'}); - } - - return returnValue; }); // event before Detach connection newJsPlumbInstance.bind('beforeDetach', function(info) { - return true; }); - activeInstances[mapId] = newJsPlumbInstance; console.log('new jsPlumbInstance: ' + mapId); @@ -2901,7 +3077,7 @@ define([ * load OR updates system map * @param mapConfig */ - $.fn.loadMap = function(mapConfig){ + $.fn.loadMap = function(mapConfig, options){ // parent element where the map will be loaded var parentElement = $(this); @@ -2940,11 +3116,12 @@ define([ return false; } - // show nice visualization effect - mapContainer.visualizeMap('show', function(){ - switchTabCallback( mapConfig.config.name ); - }); - + if(options.showAnimation){ + // show nice visualization effect + mapContainer.visualizeMap('show', function(){ + switchTabCallback( mapConfig.config.name ); + }); + } }); }; diff --git a/js/app/module_map.js b/js/app/module_map.js index 65cb1b47..09c20105 100644 --- a/js/app/module_map.js +++ b/js/app/module_map.js @@ -4,13 +4,15 @@ define([ 'app/util', 'app/render', 'bootbox', - 'morris', - //'datatables', + 'app/ui/system_info', + 'app/ui/system_graph', + 'app/ui/system_route', + 'app/ui/system_killboard', 'datatablesTableTools', 'xEditable', 'app/map/map', 'app/counter' -], function($, Config, Util, Render, bootbox, Morris) { +], function($, Config, Util, Render, bootbox) { 'use strict'; @@ -29,9 +31,12 @@ define([ mapClass: 'pf-map', // class for each map // dialogs - newMapDialogId: 'pf-map-new-dialog', // id for system dialog signatureReaderDialogId: 'pf-signature-reader-dialog', // id for signature reader dialog + // tables + tableToolsClass: 'pf-table-tools', // table toolbar + tableToolsActionClass: 'pf-table-tools-action', // table toolbar action + // TabContentStructure mapTabContentRow: 'pf-map-content-row', // main row for Tab content (grid) mapTabContentCell: 'pf-map-content-col', // column @@ -42,35 +47,20 @@ define([ moduleClass: 'pf-module', // class for each module // system info module - systemInfoModuleClass: 'pf-system-info-module', // module wrapper - systemInfoRoutesClass: 'pf-system-info-routes', // wrapper for trade hub routes - systemInfoGraphsClass: 'pf-system-info-graphs', // wrapper for graphs - systemInfoGraphKillsClass: 'pf-system-info-graph-kills', // class for system kill graph - systemInfoTableClass: 'pf-system-info-table', // class for system info table - systemInfoTableEffectRowClass: 'pf-system-info-effect-row', // class for system info table effect row - systemInfoRoutesTableClass: 'pf-system-route-table', // class for route tables - systemInfoRoutesTableRowPrefix: 'pf-system-info-routes-row-', // prefix class for a row in the route table - systemSecurityClassPrefix: 'pf-system-security-', // prefix class for system security level (color) - systemInfoProgressScannedClass: 'pf-system-progress-scanned', // progress bar scanned signatures // sig table module sigTableModuleClass: 'pf-sig-table-module', // module wrapper - sigTableToolsClass: 'pf-sig-table-tools', // table toolbar - sigTableToolsActionClass: 'pf-sig-table-tools-action', // table toolbar action sigTableClass: 'pf-sig-table', // Table class for all Signature Tables sigTableMainClass: 'pf-sig-table-main', // Table class for main sig table sigTableEditText: 'pf-sig-table-edit-text', // class for editable fields (text) sigTableEditSigNameInput: 'pf-sig-table-edit-name-input', // class for editable fields (input) sigTableEditSigTypeSelect: 'pf-sig-table-edit-type-select', // class for editable fields (select) sigTableEditSigNameSelect: 'pf-sig-table-edit-name-select', // class for editable fields (select) - sigTableCounterClass: 'pf-sig-table-counter', // class for signature table counter + sigTableCounterClass: 'pf-sig-table-counter' // class for signature table counter }; - var cache = { - systemRoutes: {}, // jump information between solar systems - systemKillsGraphData: {} // data for system kills info graph - }; + var mapTabChangeBlocked = false; // flag for preventing map tab switch @@ -122,7 +112,7 @@ define([ // collect all relevant data for SystemInfoElement var systemInfoData = { - systemId: parseInt( $( mapData.system).data('id') ), + systemData: $( mapData.system).getSystemData(), mapId: parseInt( $( mapData.system).attr('data-mapid') ) }; @@ -259,7 +249,7 @@ define([ // add toolbar buttons for table ------------------------------------- var tableToolbar = $('
', { - class: config.sigTableToolsClass + class: config.tableToolsClass }).append( $('
', { - class: ['compact', 'stripe', 'order-column', 'row-border', config.systemInfoRoutesTableClass].join(' ') - }); - - parentElement.append( $(table) ); - - // init empty table - var routesTable = table.DataTable( { - paging: false, - ordering: true, - order: [ 1, 'asc' ], - info: false, - searching: false, - hover: false, - - //autoWidth: false, - columnDefs: [ - { - targets: 0, - //"orderData": 0, - orderable: true, - title: 'system' - },{ - targets: 1, - orderable: true, - title: 'jumps    ', - width: '40px', - class: 'text-right' - },{ - targets: 2, - orderable: false, - title: 'route' - } - ], - data: [] // will be added dynamic - } ); - - $.each(systemsTo, function(i, systemTo){ - - if(systemFrom !== systemTo){ - - var cacheKey = systemFrom + '_' + systemTo; - - // row class - var rowClass = config.systemInfoRoutesTableRowPrefix + i; - - if(cache.systemRoutes.hasOwnProperty(cacheKey)){ - // add new row from cache - routesTable.row.add( cache.systemRoutes[cacheKey] ).draw().nodes().to$().addClass( rowClass ); - - // init tooltips for each jump system - var tooltipElements = parentElement.find('.' + rowClass + ' [data-toggle="tooltip"]'); - $(tooltipElements).tooltip(); - }else{ - // get route from API - var baseUrl = Config.url.eveCentral + 'route/from/'; - - var url = baseUrl + systemFrom + '/to/' + systemTo; - - $.getJSON(url, function(routeData){ - - // add row Data - var rowData = [systemTo, routeData.length]; - - var jumpData = []; - // loop all systems on a rout - $.each(routeData, function(j, systemData){ - - var systemSecClass = config.systemSecurityClassPrefix; - var systemSec = systemData.to.security.toFixed(1).toString(); - systemSecClass += systemSec.replace('.', '-'); - var system = ''; - jumpData.push( system ); - - }); - - - rowData.push( jumpData.join(' ') ); - - cache.systemRoutes[cacheKey] = rowData; - - // add new row - routesTable.row.add( cache.systemRoutes[cacheKey] ).draw().nodes().to$().addClass( rowClass ); - - // init tooltips for each jump system - var tooltipElements = parentElement.find('.' + rowClass + ' [data-toggle="tooltip"]'); - $(tooltipElements).tooltip(); - - }); - - } - - } - - }); - - }; - - /** * update Progressbar for all scanned signatures in a system */ @@ -1726,7 +1281,6 @@ define([ // update map if(tempMapUserDataClone){ - //console.log('User: ' + tempMapUserDataClone.data.systems[0].user.length); mapElement.updateUserData(tempMapUserDataClone, currentUserData); } } @@ -1749,7 +1303,7 @@ define([ }) ).append( $('
', { - class: ['col-xs-6', 'col-md-4', config.mapTabContentCellSecond, config.mapTabContentCell].join(' ') + class: ['col-xs-12', 'col-md-4', config.mapTabContentCellSecond, config.mapTabContentCell].join(' ') }) ); @@ -1760,7 +1314,7 @@ define([ }; - var getTabElementh = function(options){ + var getTabElement = function(options){ var tabElement = $('
', { id: config.mapTabElementId @@ -1781,6 +1335,34 @@ define([ return tabElement; }; + /** + * set data for a map tab, or update an existing map tab with new data + * @param options + */ + $.fn.updateTabData = function(options){ + var tabElement = $(this); + + // set main data + tabElement.data('map-id', options.id).data('updated', options.updated); + + // change tab link + tabElement.attr('href', '#' + config.mapTabIdPrefix + options.id); + + // change map icon + tabElement.find('i').removeClass().addClass(['fa', 'fa-fw', options.icon].join(' ')); + + // change map name label + tabElement.find('.' + config.mapTabLinkTextClass).text(options.name); + + // change tabClass + var listElement = tabElement.parent(); + tabElement.parent().removeClass().addClass([config.mapTabClass, options.type.classTab ].join(' ')); + if(options.right === true){ + listElement.addClass('pull-right'); + } + }; + + /** * add a new tab to tab-map-module end returns the new objects * @param options @@ -1792,9 +1374,7 @@ define([ var tabBar = tabElement.find('ul.nav-tabs'); var tabContent = tabElement.find('div.tab-content'); - var listElement = $('
  • ', { - class: options.tabClasses.join(' ') - }).attr('role', 'presentation'); + var listElement = $('
  • ').attr('role', 'presentation'); if(options.active === true){ listElement.addClass('active'); @@ -1805,19 +1385,14 @@ define([ } // link element ------- - var linkElement = $('', { - href: '#' + config.mapTabIdPrefix + options.id - }).attr('role', 'tab').data('map-id', options.id); + var linkElement = $('').attr('role', 'tab'); // icon element ------ - var iconElement = $('', { - class: ['fa', 'fa-fw', options.icon].join(' ') - }); + var iconElement = $(''); // text element ----- var textElement = $('', { - class: config.mapTabLinkTextClass, - text: options.name + class: config.mapTabLinkTextClass }); var newListElement = listElement.append( @@ -1826,6 +1401,9 @@ define([ tabBar.append( newListElement ); + // update Tab element -> set data + linkElement.updateTabData(options); + // tabs content ==================================== var contentElement = $('
    ', { id: config.mapTabIdPrefix + options.id, @@ -1858,21 +1436,27 @@ define([ var tabLinkElement = $(this); var mapId = tabLinkElement.data('map-id'); - var mapElement = $('#' + config.mapTabElementId).getActiveMap(); + // ignore "add" tab. no need for map change + if(mapId > 0){ + var mapElement = $('#' + config.mapTabElementId).getActiveMap(); - if(mapId !== mapElement.data('id')){ - // block tabs until switch is done - mapTabChangeBlocked = true; + if(mapId !== mapElement.data('id')){ + // block tabs until switch is done + mapTabChangeBlocked = true; - // freeze active map -> no user data update while map switch - mapElement.data('frozen', true); + // freeze active map -> no user data update while map switch + mapElement.data('frozen', true); - // hide current map with animation - mapElement.visualizeMap('hide', function(){ - // un-block map tabs - mapTabChangeBlocked = switchTabCallback(mapElement, tabLinkElement); - }); + // hide current map with animation + mapElement.visualizeMap('hide', function(){ + // un-block map tabs + mapTabChangeBlocked = switchTabCallback(mapElement, tabLinkElement); + }); + } + }else{ + tabLinkElement.tab('show'); } + } }); @@ -1945,6 +1529,11 @@ define([ */ $.fn.updateMapModule = function(mapData){ + if(mapData.length === 0){ + return true; + } + + // update current map data currentMapData = mapData; @@ -1968,27 +1557,30 @@ define([ // check whether a tab/map is still active for(var i = 0; i < tabElements.length; i++){ - var mapId = $(tabElements[i]).data('map-id'); + var tabElement = $(tabElements[i]); + var mapId = tabElement.data('map-id'); if(mapId > 0){ var tabMapData = getMapDataById(mapId); if(tabMapData !== false){ - // map data available -> update map + // map data available -> activeMapIds.push(mapId); + // check for map data change and update tab + if(tabMapData.config.updated !== tabElement.data('updated')){ + tabElement.updateTabData(tabMapData.config); + } }else{ - - tabsChanged = true; - // map data not available -> remove tab var deletedTabName = tabMapElement.deleteTab(mapId); + tabsChanged = true; + if(deletedTabName.length > 0){ Util.showNotify({title: 'Map removed', text: deletedTabName + ' deleted', type: 'warning'}); } - } } } @@ -2001,12 +1593,13 @@ define([ var tabOptions = { id: parseInt( data.config.id ), - tabClasses: [config.mapTabClass, Util.getInfoForMap( data.config.type.name, 'classTab') ], + type: data.config.type, contentClasses: [config.mapTabContentClass], active: false, icon: data.config.icon, name: data.config.name, - right: false + right: false, + updated: data.config.updated }; var newTabElements = tabMapElement.addTab(tabOptions); @@ -2017,12 +1610,22 @@ define([ // load all the structure elements for the new tab newTabElements.contentElement.initContentStructure(); - Util.showNotify({title: 'Map added', text: data.config.name + ' added', type: 'success'}); + tabsChanged = true; + Util.showNotify({title: 'Map added', text: data.config.name + ' added', type: 'success'}); } }); + // get current active map + var activeMapId = Util.getMapModule().getActiveMap().data('id'); + var activeMapData = getMapDataById(activeMapId); + + if(activeMapData !== false){ + // update active map with new mapData + var currentTabContentElement = $('#' + config.mapTabIdPrefix + activeMapId); + $( currentTabContentElement).loadMap( activeMapData, {} ); + } }else{ // create Tab Element @@ -2032,7 +1635,7 @@ define([ barId: config.mapTabBarId }; - tabMapElement = getTabElementh(options); + tabMapElement = getTabElement(options); // add new tab for each map for(var j = 0; j < tempMapData.length; j++){ @@ -2046,12 +1649,13 @@ define([ var tabOptions = { id: parseInt( data.config.id ), - tabClasses: [config.mapTabClass, Util.getInfoForMap( data.config.type.name, 'classTab') ], + type: data.config.type, contentClasses: [config.mapTabContentClass], active: activeTab, icon: data.config.icon, name: data.config.name, - right: false + right: false, + updated: data.config.updated }; tabMapElement.addTab(tabOptions); @@ -2061,7 +1665,9 @@ define([ // add "add" button var tabAddOptions = { id: 0, - tabClasses: [config.mapTabClass, Util.getInfoForMap( 'standard', 'classTab') ], + type: { + classTab: Util.getInfoForMap( 'standard', 'classTab') + }, contentClasses: [config.mapTabContentClass], icon: 'fa-plus', name: 'add', @@ -2085,7 +1691,7 @@ define([ tabContentElements.initContentStructure(); // load first map i in first tab content container - $( tabContentElements[0] ).loadMap( tempMapData[0] ); + $( tabContentElements[0] ).loadMap( tempMapData[0], {showAnimation: true} ); } if(tabsChanged === true){ @@ -2117,7 +1723,7 @@ define([ // load map var currentTabContentElement = $('#' + config.mapTabIdPrefix + mapId); - $( currentTabContentElement).loadMap( tabMapData ); + $( currentTabContentElement).loadMap( tabMapData, {showAnimation: true} ); // "wake up" scrollbar for map and get previous state back var scrollableElement = currentTabContentElement.find('.' + config.mapWrapperClass); diff --git a/js/app/page.js b/js/app/page.js index 4acba84e..458fcec9 100644 --- a/js/app/page.js +++ b/js/app/page.js @@ -50,6 +50,9 @@ define([ dialogNavigationClass: 'pf-dialog-navigation-list', // class for dialog navigation bar dialogNavigationListItemClass: 'pf-dialog-navigation-list-item', // class for map manual li main navigation elements + // map dialog + newMapDialogId: 'pf-map-new-dialog', // id for edit/update map dialog + // system effect dialog systemEffectDialogWrapperClass: 'pf-system-effect-dialog-wrapper', // class for system effect dialog @@ -267,6 +270,17 @@ define([ ).on('click', function(){ $(document).triggerMenuEvent('ShowTaskManager'); }) + ).append( + $('', { + class: 'list-group-item', + href: '#' + }).html('  Delete').prepend( + $('',{ + class: 'fa fa-eraser fa-fw' + }) + ).on('click', function(){ + $(document).triggerMenuEvent('DeleteMap'); + }) ) ); }; @@ -437,6 +451,20 @@ define([ return false; }); + $(document).on('pf:menuDeleteMap', function(e){ + // delete current active map + var mapData = false; + + var activeMap = Util.getMapModule().getActiveMap(); + + if(activeMap){ + mapData = activeMap.getMapData(true); + } + + showDeleteMapDialog(mapData); + return false; + }); + $(document).on('pf:menuShowTaskManager', function(e, data){ // show log dialog Logging.showDialog(); @@ -554,65 +582,126 @@ define([ }; /** - * shows the add new map dialog + * shows the add/edit map dialog */ var showNewMapDialog = function(mapData){ var formData = {}; - requirejs(['text!templates/modules/map_dialog.html', 'mustache'], function(template, Mustache) { + // check if dialog is already open + var mapInfoDialogElement = $('#' + config.newMapDialogId); + if(!mapInfoDialogElement.is(':visible')){ - var data = { - id: Init.newMapDialogId, - scope: Util.getMapScopes(), - type: Util.getMapTypes(), - icon: Util.getMapIcons(), - formData: formData - }; - console.log(data); - var content = Mustache.render(template, data); + requirejs(['text!templates/modules/map_dialog.html', 'mustache'], function(template, Mustache) { - var dialogTitle = 'New map'; - var dialogSaveButton = 'add map'; - if(mapData !== false){ - dialogTitle = 'Edit map'; - dialogSaveButton = 'save map'; + var data = { + id: config.newMapDialogId, + scope: Util.getMapScopes(), + type: Util.getMapTypes(), + icon: Util.getMapIcons(), + formData: formData + }; - content = $(content); - content.find('select[name="icon"]').val( mapData.config.icon ); - content.find('input[name="name"]').val( mapData.config.name ); - content.find('select[name="scope"]').val( mapData.config.scope ); - content.find('select[name="type"]').val( mapData.config.type ); - } + var content = Mustache.render(template, data); + var dialogTitle = 'Create new map'; + var dialogSaveButton = 'add map'; + if(mapData !== false){ + dialogTitle = 'Edit map'; + dialogSaveButton = 'save map'; + content = $(content); + content.find('input[name="id"]').val( mapData.config.id ); + content.find('select[name="icon"]').val( mapData.config.icon ); + content.find('input[name="name"]').val( mapData.config.name ); + content.find('select[name="scopeId"]').val( mapData.config.scope.id ); + content.find('select[name="typeId"]').val( mapData.config.type.id ); + } - var mapInfoDialog = bootbox.dialog({ - title: dialogTitle, - message: content, - buttons: { - close: { - label: 'cancel', - className: 'btn-default' - }, - success: { - label: '' + dialogSaveButton, - className: 'btn-primary', - callback: function() { + var mapInfoDialog = bootbox.dialog({ + title: dialogTitle, + message: content, + buttons: { + close: { + label: 'cancel', + className: 'btn-default' + }, + success: { + label: '' + dialogSaveButton, + className: 'btn-primary', + callback: function() { - // get form Values - var form = $('#' + config.newMapDialogId).find('form'); - var newMapData = form.getFormValues(); + // get form Values + var form = $('#' + config.newMapDialogId).find('form'); - // TODO save map data + // validate form + form.validator('validate'); + + // check weather the form is valid + var formValid = form.isValidForm(); + + if(formValid === true){ + + var newMapData = {mapData: form.getFormValues()}; + + $.ajax({ + type: 'POST', + url: Init.path.saveMap, + data: newMapData, + dataType: 'json' + }).done(function(data){ + Util.showNotify({title: dialogTitle, text: 'Map: ' + data.name, type: 'success'}); + + $(mapInfoDialog).modal('hide'); + $(document).trigger('pf:closeMenu', [{}]); + }).fail(function( jqXHR, status, error) { + var reason = status + ' ' + error; + Util.showNotify({title: jqXHR.status + ': saveMap', text: reason, type: 'warning'}); + $(document).setProgramStatus('problem'); + }); + } + + return false; + } } } - } + }); }); - - }); + } }; + /** + * shows the delete map Dialog + * @param mapElement + */ + var showDeleteMapDialog = function(mapData){ + var mapName = mapData.config.name; + + var mapDeleteDialog = bootbox.confirm('Delete map "' + mapName + '"?', function(result){ + if(result){ + + var data = {mapData: mapData.config}; + + $.ajax({ + type: 'POST', + url: Init.path.deleteMap, + data: data, + dataType: 'json' + }).done(function(data){ + Util.showNotify({title: 'Map deleted', text: 'Map: ' + mapName, type: 'success'}); + + $(mapDeleteDialog).modal('hide'); + }).fail(function( jqXHR, status, error) { + var reason = status + ' ' + error; + Util.showNotify({title: jqXHR.status + ': deleteMap', text: reason, type: 'warning'}); + $(document).setProgramStatus('problem'); + }); + + return false; + } + }); + + }; /** * shows the map manual modal dialog @@ -925,6 +1014,15 @@ define([ // change status, on status changed if(iconClass !== false){ + // "problem" and "offline" always have priority -> ignore/clear interval + if( + status === 'problem' || + status === 'offline' + ){ + clearInterval(programStatusInterval); + programStatusInterval = false; + } + if(! statusElement.hasClass(textClass) ){ if(! programStatusInterval){ @@ -948,7 +1046,8 @@ define([ }); } - programStatusCounter--; + // decrement counter + programStatusCounter -= 1000; if(programStatusCounter <= 0){ clearInterval(programStatusInterval); diff --git a/js/app/ui/form_element.js b/js/app/ui/form_element.js new file mode 100644 index 00000000..c2b5514e --- /dev/null +++ b/js/app/ui/form_element.js @@ -0,0 +1,76 @@ +/** + * form elements + */ + +define([ + 'jquery', + 'app/init', + 'app/util' +], function($, Init, Util) { + "use strict"; + + /** + * init a select element as an ajax based "select2" object + * @param options + */ + $.fn.initSystemSelect = function(options){ + var selectElement = $(this); + + + $.when( + selectElement.select2({ + ajax: { + url: function(params){ + // add params to URL + return Init.path.searchSystem + '/' + params.term; + }, + dataType: 'json', + delay: 250, + timeout: 5000, + cache: true, + data: function(params) { + // no url params here + return; + }, + processResults: function(data) { + // parse the results into the format expected by Select2. + return { + results: data.map( function(item){ + + var secClass = Util.getSecurityClassForSystem(item.security); + + var systemSecurity = ' '; + systemSecurity += '(' + item.security + ')'; + + return { + id: item[options.key], + text: item.name + systemSecurity + }; + }) + }; + }, + error: function (jqXHR, status, error) { + if(!Util.isXHRAborted){ + // close select + selectElement.select2('destroy'); + + var reason = status + ' ' + jqXHR.status + ': ' + error; + Util.emergencyShutdown(reason); + } + + } + }, + minimumInputLength: 2, + placeholder: 'Jita', + allowClear: true + }) + ).done(function(){ + // open select + selectElement.select2('open'); + }); + + + }; + + +}); \ No newline at end of file diff --git a/js/app/ui/map_info.js b/js/app/ui/map_info.js index bc8873a2..5e905347 100644 --- a/js/app/ui/map_info.js +++ b/js/app/ui/map_info.js @@ -43,6 +43,16 @@ define([ var countSystems = mapData.data.systems.length; var countConnections = mapData.data.connections.length; + // map type + var mapTypes = Util.getMapTypes(); + var mapTypeName = ''; + var mapTypeClass = ''; + for(var i = 0; i < mapTypes.length; i++){ + if(mapTypes[i].id === mapData.config.type.id){ + mapTypeName = mapTypes[i].name; + mapTypeClass = mapTypes[i].class; + } + } var dlElementLeft = $('
    ', { class: 'dl-horizontal', @@ -63,8 +73,8 @@ define([ $('
    ').text( 'Type' ) ).append( $('
    ', { - class: Util.getInfoForMap( mapData.config.type, 'class') - }).text( Util.getInfoForMap( mapData.config.type, 'label') ) + class: mapTypeClass + }).text( mapTypeName ) ); mapElement.append(dlElementLeft); @@ -129,7 +139,7 @@ define([ } // type - tempData.push(tempSystemData.type); + tempData.push(Util.getSystemTypeInfo(tempSystemData.type.id, 'name')); // name tempData.push( tempSystemData.name ); @@ -142,7 +152,7 @@ define([ } // status - var systemStatusClass = Util.getStatusInfoForSystem(tempSystemData.status, 'class'); + var systemStatusClass = Util.getStatusInfoForSystem(tempSystemData.status.id, 'class'); if(systemStatusClass !== ''){ tempData.push( '' ); }else{ @@ -166,14 +176,14 @@ define([ } // locked - if(tempSystemData.locked === true){ + if(tempSystemData.locked === 1){ tempData.push( '' ); }else{ tempData.push( '' ); } // rally point - if(tempSystemData.rally === true){ + if(tempSystemData.rally === 1){ tempData.push( '' ); }else{ tempData.push( '' ); @@ -274,7 +284,7 @@ define([ var tempConData = []; - tempConData.push( Util.getScopeInfoForMap( tempConnectionData.scope, 'label') ); + tempConData.push( Util.getScopeInfoForConnection( tempConnectionData.scope, 'label') ); // source system name tempConData.push( tempConnectionData.sourceName ); diff --git a/js/app/ui/system_graph.js b/js/app/ui/system_graph.js new file mode 100644 index 00000000..0663c222 --- /dev/null +++ b/js/app/ui/system_graph.js @@ -0,0 +1,211 @@ +/** + * System graph module + */ + +define([ + 'jquery', + 'app/init', + 'app/util', + 'morris' +], function($, Init, Util, Morris) { + 'use strict'; + + var config = { + // module info + moduleClass: 'pf-module', // class for each module + + // system graph module + systemGraphModuleClass: 'pf-system-graph-module', // class for this module + systemGraphClass: 'pf-system-graph', // class for each graph + + // system graph labels + systemGraphLabels: { + jumps: { + headline: 'Jumps', + units: 'jumps', + ykeys: ['y'], + labels: ['jumps'], + lineColors: ['#375959'], + pointFillColors: ['#477372'] + }, + shipKills: { + headline: 'Ship/POD Kills', + units: 'kills', + ykeys: ['y', 'z'], + labels: ['Ship kills', 'POD kills'], + lineColors: ['#375959', '#477372'], + pointFillColors: ['#477372', '#568a89'] + }, + factionKills: { + headline: 'NPC Kills', + units: 'kills', + ykeys: ['y'], + labels: ['kills'], + lineColors: ['#375959'], + pointFillColors: ['#477372'] + } + } + }; + + /** + * get info for a given graph key + * @param graphKey + * @param option + * @returns {string} + */ + var getInfoForGraph = function(graphKey, option){ + var info = ''; + + if(config.systemGraphLabels.hasOwnProperty(graphKey)){ + info = config.systemGraphLabels[graphKey][option]; + } + + return info; + }; + + /** + * init Morris Graph + * @param graphElement + * @param graphKey + * @param graphData + */ + var initGraph = function(graphElement, graphKey, graphData){ + + if(graphData.length > 0){ + var labelYFormat = function(y){ + return Math.round(y); + }; + + Morris.Area({ + element: graphElement, + data: graphData, + xkey: 'x', + ykeys: getInfoForGraph(graphKey, 'ykeys'), + labels: getInfoForGraph(graphKey, 'labels'), + parseTime: false, + ymin: 0, + yLabelFormat: labelYFormat, + padding: 10, + hideHover: true, + pointSize: 3, + lineColors: getInfoForGraph(graphKey, 'lineColors'), + pointFillColors: getInfoForGraph(graphKey, 'pointFillColors'), + pointStrokeColors: ['#141413'], + lineWidth: 2, + grid: true, + gridStrokeWidth: 0.3, + gridTextSize: 9, + gridTextFamily: 'Oxygen Bold', + gridTextColor: '#63676a', + behaveLikeLine: false, + goals: [], + goalLineColors: ['#5cb85c'], + smooth: true, + fillOpacity: 0.2, + resize: true, + redraw: true + }); + } + }; + + /** + * draw graph module + * @param parentElement + * @param systemData + */ + var drawModule = function(parentElement, systemData){ + + // graph data is available for k-space systems + if(systemData.type.id === 2){ + var requestData = { + systemIds: [systemData.systemId] + }; + + $.ajax({ + type: 'POST', + url: Init.path.getSystemGraphData, + data: requestData, + dataType: 'json' + }).done(function(systemGraphsData){ + + // create new (hidden) module container + var moduleElement = $('
    ', { + class: [config.moduleClass, config.systemGraphModuleClass].join(' '), + css: {opacity: 0} + }); + parentElement.append(moduleElement); + + // row element + var rowElement = $('
    ', { + class: 'row' + }); + moduleElement.append(rowElement); + + $.each(systemGraphsData, function(systemId, graphsData){ + $.each(graphsData, function(graphKey, graphData){ + + var colElement = $('
    ', { + class: ['col-xs-12', 'col-sm-6', 'col-md-4'].join(' ') + }); + + var headlineElement = $('
    ').text( getInfoForGraph(graphKey, 'headline') ); + + colElement.append(headlineElement); + + var graphElement = $('
    ', { + class: config.systemGraphClass + }); + + colElement.append(graphElement); + + rowElement.append(colElement); + initGraph(graphElement, graphKey, graphData); + }); + }); + + moduleElement.append($('
    ', { + css: {'clear': 'both'} + })); + + // show module + moduleElement.velocity('stop').velocity('transition.slideUpIn', { + queue: false, + duration: Init.animationSpeed.mapModule + }); + + }).fail(function( jqXHR, status, error) { + var reason = status + ' ' + error; + Util.showNotify({title: jqXHR.status + ': System graph data', text: reason, type: 'warning'}); + $(document).setProgramStatus('problem'); + }); + } + + }; + + + /** + * main module load function + * @param systemData + */ + $.fn.drawSystemGraphModule = function(systemData){ + + var parentElement = $(this); + + // check if module already exists + var moduleElement = parentElement.find('.' + config.systemGraphModuleClass); + + if(moduleElement.length > 0){ + moduleElement.velocity('stop').velocity('reverse', { + complete: function(tempElement){ + $(tempElement).remove(); + drawModule(parentElement, systemData); + } + }); + }else{ + drawModule(parentElement, systemData); + } + }; + + + +}); diff --git a/js/app/ui/system_info.js b/js/app/ui/system_info.js new file mode 100644 index 00000000..d96515ea --- /dev/null +++ b/js/app/ui/system_info.js @@ -0,0 +1,137 @@ +/** + * System info module + */ + +define([ + 'jquery', + 'app/init', + 'app/util', + 'app/render' +], function($, Init, Util, Render) { + "use strict"; + + var config = { + // module info + moduleClass: 'pf-module', // class for each module + + // system info module + systemInfoModuleClass: 'pf-system-info-module', // module wrapper + systemInfoTableClass: 'pf-system-info-table' // class for system info table + }; + + /** + * + * @param parentElement + * @param systemInfoData + */ + var drawModule = function(parentElement, systemData){ + + // create new module container + var moduleElement = $('
    ', { + class: [config.moduleClass, config.systemInfoModuleClass].join(' '), + css: {opacity: 0} + }); + + parentElement.prepend(moduleElement); + + var effectName = Util.getEffectInfoForSystem(systemData.effect, 'name'); + var effectClass = Util.getEffectInfoForSystem(systemData.effect, 'class'); + + // systemInfo template config + var moduleConfig = { + name: 'modules/system_info', + position: moduleElement, + link: 'append', + functions: { + after: function(){ + // init tooltips + var tooltipElements = $('.' + config.systemInfoModuleClass + ' [data-toggle="tooltip"]'); + tooltipElements.tooltip(); + + // init system effect popover + var systemEffectData = Util.getSystemEffectData( systemData.security, systemData.effect); + + if(systemEffectData !== false){ + + var systemInfoTable = $(moduleElement).find('.' + config.systemInfoTableClass); + + // transform data into table + var systemEffectTable = Util.getSystemEffectTable( systemEffectData ); + + systemInfoTable.popover({ + html: true, + trigger: 'hover', + placement: 'top', + delay: 200, + title: 'System effects', + content: systemEffectTable + }); + } + + showModule(moduleElement); + } + } + }; + + // add security class for statics + if(systemData.static){ + $.each(systemData.static, function(i, staticWH){ + system['static'][i]['class'] = Util.getSecurityClassForSystem( staticWH.security ); + }); + } + + var moduleData = { + system: systemData, + tableClass: config.systemInfoTableClass, + systemTypeName: Util.getSystemTypeInfo(systemData.type.id, 'name'), + securityClass: Util.getSecurityClassForSystem( systemData.security ), + trueSec: systemData.trueSec.toFixed(1), + trueSecClass: Util.getTrueSecClassForSystem( systemData.trueSec ), + effectName: effectName, + effectClass: effectClass + }; + + Render.showModule(moduleConfig, moduleData); + }; + + /** + * show system info module with animation + * @param moduleElement + */ + var showModule = function(moduleElement){ + moduleElement.velocity('stop').velocity('transition.slideUpIn', { + queue: false, + duration: Init.animationSpeed.mapModule + }); + }; + + /** + * update system info module + * @param systemInfoData + */ + $.fn.drawSystemInfoModule = function(systemData){ + + var parentElement = $(this); + + // check if module already exists + var moduleElement = parentElement.find('.' + config.systemInfoModuleClass); + + if(moduleElement.length > 0){ + moduleElement.velocity('stop').velocity('reverse', { + complete: function(tempElement){ + $(tempElement).remove(); + + drawModule(parentElement, systemData); + } + }); + }else{ + drawModule(parentElement, systemData); + } + + + + }; +}); + + + diff --git a/js/app/ui/system_killboard.js b/js/app/ui/system_killboard.js new file mode 100644 index 00000000..9a8a33e0 --- /dev/null +++ b/js/app/ui/system_killboard.js @@ -0,0 +1,291 @@ +define([ + 'jquery', + 'app/init', + 'app/util', + 'morris' +], function($, Init, Util, Morris) { + "use strict"; + + var config = { + // module info + moduleClass: 'pf-module', // class for each module + + // system killboard module + systemKillboardModuleClass: 'pf-system-killboard-module', // module wrapper + systemKillboardGraphsClass: 'pf-system-killboard-graphs', // wrapper for graph + systemKillboardGraphKillsClass: 'pf-system-killboard-graph-kills' // class for system kill graph + + }; + + var cache = { + systemKillsGraphData: {} // data for system kills info graph + }; + + /** + * get label element with given content + * @param text + * @returns {*|XMLList} + */ + var getLabel = function(text, options){ + var label = $('', { + class: ['label', options.type, options.align].join(' ') + }).text( text ); + + return label; + }; + + /** + * updates the system info graph + * @param systemId + */ + $.fn.updateSystemInfoGraphs = function(systemId){ + + var parentElement = $(this); + + var graphElement = $('
    ', { + class: config.systemKillboardGraphKillsClass + }); + + parentElement.append(graphElement); + + var showHours = 24; + var maxKillmailCount = 200; // limited by API + + var labelOptions = { + align: 'center-block' + }; + + // private function draws a "system kills" graph + var drawGraph = function(data){ + + var tableData = data.tableData; + var label = ''; + + if(data.count === 0){ + labelOptions.type = 'label-success'; + label = getLabel( 'No kills found within 24h', labelOptions ); + graphElement.prepend( label ); + + // reduce height + graphElement.velocity({ + height: '30px' + },{ + duration: Init.animationSpeed.mapModule + }); + + return; + } + + var labelYFormat = function(y){ + return Math.round(y); + }; + + // draw chart + Morris.Bar({ + element: graphElement, + resize: true, + grid: true, + gridStrokeWidth: 0.3, + gridTextSize: 9, + gridTextColor: '#63676a', + gridTextFamily: 'Oxygen Bold', + hideHover: true, + data: tableData, + xkey: 'label', + ykeys: ['kills'], + labels: ['Kills'], + yLabelFormat: labelYFormat, + xLabelMargin: 10, + padding: 10, + parseTime: false, + barOpacity: 0.8, + barRadius: [2, 2, 0, 0], + barSizeRatio: 0.5, + barGap: 3, + barColors: function (row, series, type) { + if (type === 'bar') { + // highlight last row -> recent kills found + if(this.xmax === row.x){ + return '#c2760c'; + } + } + + return '#375959'; + } + }); + + // show hint for recent kills + if(tableData[tableData.length - 1].kills > 0){ + labelOptions.type = 'label-warning'; + label = getLabel( tableData[tableData.length - 1].kills + ' kills within the last hour!', labelOptions ); + graphElement.prepend( label ); + } + }; + + // get recent KB stats (last 24h)) + var localDate = new Date(); + + // cache result for 5min + var cacheKey = systemId + '_' + localDate.getHours() + '_' + ( Math.ceil( localDate.getMinutes() / 5 ) * 5); + + if(cache.systemKillsGraphData.hasOwnProperty(cacheKey) ){ + drawGraph( cache.systemKillsGraphData[cacheKey] ); + }else{ + + // chart data + var chartData = []; + + for(var i = 0; i < showHours; i++){ + var tempData = { + label: i + 'h', + kills: 0 + }; + + chartData.push(tempData); + } + + // get current server time + var serverDate= Util.getServerTime(); + + // get all kills until current server time + var dateStringEnd = String( serverDate.getFullYear() ); + dateStringEnd += String( ('0' + (serverDate.getMonth() + 1)).slice(-2) ); + dateStringEnd += String( ('0' + serverDate.getDate()).slice(-2) ); + dateStringEnd += String( ('0' + serverDate.getHours()).slice(-2) ); + dateStringEnd += String( ('0' + serverDate.getMinutes()).slice(-2) ); + + // get start Date for kills API request (last 24h) + var startDate = new Date( serverDate.getTime() ); + startDate.setDate( startDate.getDate() - 1); + var dateStringStart = String( startDate.getFullYear() ); + dateStringStart += String( ('0' + (startDate.getMonth() + 1)).slice(-2) ); + dateStringStart += String( ('0' + startDate.getDate()).slice(-2) ); + dateStringStart += String( ('0' + startDate.getHours()).slice(-2) ); + dateStringStart += String( ('0' + startDate.getMinutes()).slice(-2) ); + + var url = Init.url.zKillboard; + url += '/no-items/no-attackers/solarSystemID/' + systemId + '/startTime/' + dateStringStart + '/endTime/' + dateStringEnd + '/'; + + graphElement.showLoadingAnimation(); + + $.getJSON(url, function(kbData){ + + // the API wont return more than 200KMs ! - remember last bar block with complete KM information + var lastCompleteDiffHourData = 0; + + + // loop kills and count kills by hour + for(var i = 0; i < kbData.length; i++){ + var match = kbData[i].killTime.match(/^(\d+)-(\d+)-(\d+) (\d+)\:(\d+)\:(\d+)$/); + var killDate = new Date(match[1], match[2] - 1, match[3], match[4], match[5], match[6]); + + // get time diff + var timeDiffMin = Math.round( ( serverDate - killDate ) / 1000 / 60 ); + var timeDiffHour = Math.round( timeDiffMin / 60 ); + + // update chart data + if(chartData[timeDiffHour]){ + chartData[timeDiffHour].kills++; + + if(timeDiffHour > lastCompleteDiffHourData){ + lastCompleteDiffHourData = timeDiffHour; + } + } + + } + + // remove empty chart Data + if(kbData.length >= maxKillmailCount){ + chartData = chartData.splice(0, lastCompleteDiffHourData + 1); + } + + // change order + chartData.reverse(); + + // fill cache + cache.systemKillsGraphData[cacheKey] = {}; + cache.systemKillsGraphData[cacheKey].tableData = chartData; + cache.systemKillsGraphData[cacheKey].count = kbData.length; + + drawGraph( cache.systemKillsGraphData[cacheKey] ); + + parentElement.hideLoadingAnimation(); + }).error(function(e){ + Util.showNotify({title: e.status + ': Get system kills', text: 'Loading failed', type: 'error'}); + }); + } + + }; + + /** + * get module Element + * @param systemData + * @returns {*|HTMLElement} + */ + var getModule = function(parentElement, systemData){ + + // create new module container + var moduleElement = $('
    ', { + class: [config.moduleClass, config.systemKillboardModuleClass].join(' '), + css: {opacity: 0} + }); + + // headline + var headline = $('
    ', { + text: 'Killboard' + }); + + // graph element + var graphElement = $('
    ', { + class: config.systemKillboardGraphsClass + }); + + moduleElement.append(headline, graphElement); + + parentElement.append(moduleElement); + + // update graph + graphElement.updateSystemInfoGraphs(systemData.systemId); + + return moduleElement; + }; + + + /** + * main module load function + * @param systemData + */ + $.fn.drawSystemKillboardModule = function(systemData){ + + var parentElement = $(this); + + // show route module + var showModule = function(moduleElement){ + if(moduleElement){ + + moduleElement.velocity('stop').velocity('transition.slideUpIn', { + queue: false, + duration: Init.animationSpeed.mapModule + }); + } + }; + + // check if module already exists + var moduleElement = parentElement.find('.' + config.systemKillboardModuleClass); + + if(moduleElement.length > 0){ + moduleElement.velocity('stop').velocity('reverse', { + complete: function(tempElement){ + $(tempElement).remove(); + + moduleElement = getModule(parentElement, systemData); + showModule(moduleElement); + } + }); + }else{ + moduleElement = getModule(parentElement, systemData); + showModule(moduleElement); + } + + }; +}); \ No newline at end of file diff --git a/js/app/ui/system_route.js b/js/app/ui/system_route.js new file mode 100644 index 00000000..8880ddb3 --- /dev/null +++ b/js/app/ui/system_route.js @@ -0,0 +1,383 @@ +/** + * system route module + */ + +define([ + 'jquery', + 'app/init', + 'app/util', + 'bootbox' +], function($, Init, Util, bootbox) { + 'use strict'; + + var config = { + // module info + moduleClass: 'pf-module', // class for each module + + // system route module + systemRouteModuleClass: 'pf-system-route-module', // class for this module + + // tables + tableToolsClass: 'pf-table-tools', // table toolbar + + systemSecurityClassPrefix: 'pf-system-security-', // prefix class for system security level (color) + + // dialog + routeDialogId: 'pf-route-dialog', // id for route dialog + systemDialogSelectClass: 'pf-system-dialog-select', // class for system select Element + systemInfoRoutesTableRowPrefix: 'pf-system-info-routes-row-', // prefix class for a row in the route table + systemInfoRoutesTableClass: 'pf-system-route-table' // class for route tables + + }; + + var cache = { + systemRoutes: {} // jump information between solar systems + }; + + + /** + * callback function, adds new row to a dataTable with jump information for a route + * @param context + * @param routeData + */ + var callbackAddRouteRow = function(context, routeData){ + // format routeData + var rowData = formatRouteData(context, routeData); + + cache.systemRoutes[context.cacheKey] = rowData; + + // add new row + context.dataTable.row.add( cache.systemRoutes[context.cacheKey] ).draw().nodes().to$().addClass( context.rowClass ); + + // init tooltips for each jump system + var tooltipElements = context.moduleElement.find('.' + context.rowClass + ' [data-toggle="tooltip"]'); + $(tooltipElements).tooltip(); + }; + + + /** + * show route dialog. User can search for systems and jump-info for each system is added to a data table + * @param dialogData + */ + var showFindRouteDialog = function(dialogData){ + + var data = { + id: config.routeDialogId, + selectClass: config.systemDialogSelectClass, + systemFrom: dialogData.systemFrom + }; + + requirejs(['text!templates/dialog/route.html', 'mustache'], function(template, Mustache) { + + var content = Mustache.render(template, data); + + // disable modal focus event -> otherwise select2 is not working! -> quick fix + $.fn.modal.Constructor.prototype.enforceFocus = function() {}; + + var findRouteDialog = bootbox.dialog({ + title: 'Search route', + message: content, + buttons: { + close: { + label: 'cancel', + className: 'btn-default', + callback: function(){ + $(findRouteDialog).modal('hide'); + } + }, + success: { + label: ' search route', + className: 'btn-primary', + callback: function () { + // add new route to route table + + // get form Values + var form = $('#' + config.routeDialogId).find('form'); + + var routeDialogData = $(form).getFormValues(); + + // validate form + form.validator('validate'); + + // check weather the form is valid + var formValid = form.isValidForm(); + + if(formValid === false){ + // don't close dialog + return false; + } + + // get route Data + var requestData = { + systemFrom: dialogData.systemFrom, + systemTo: routeDialogData.systemTo + }; + + // data passed into callback + var contextData = { + moduleElement: dialogData.moduleElement, + systemTo: routeDialogData.systemTo, + dataTable: dialogData.dataTable, + rowClass: config.systemInfoRoutesTableRowPrefix + dialogData.dataTable.rows().data().length, + cacheKey: dialogData.systemFrom + '_' + routeDialogData.systemTo + }; + + getRouteData(requestData, contextData, callbackAddRouteRow); + } + } + } + }); + + + // init dialog + findRouteDialog.on('shown.bs.modal', function(e) { + + var modalContent = $('#' + config.routeDialogId); + + // init system select live search + var selectElement = modalContent.find('.' + config.systemDialogSelectClass); + selectElement.initSystemSelect({key: 'name'}); + }); + + + }); + + }; + + /** + * requests route data from eveCentral API and execute callback + * @param systemFrom + * @param systemTo + * @param callback + */ + var getRouteData = function(requestData, contextData, callback){ + + // get route from API + var baseUrl = Init.url.eveCentral + 'route/from/'; + + var url = baseUrl + requestData.systemFrom + '/to/' + requestData.systemTo; + + $.ajax({ + url: url, + dataType: 'json', + context: contextData + }).done(function(routeData){ + // execute callback + callback(this, routeData); + }); + + }; + + /** + * format route data from API request into dataTable row format + * @param context + * @param routeData + * @returns {*[]} + */ + var formatRouteData = function(context, routeData){ + + // add row Data + var rowData = [context.systemTo, routeData.length]; + + var jumpData = []; + // loop all systems on a rout + $.each(routeData, function(j, systemData){ + + var systemSecClass = config.systemSecurityClassPrefix; + var systemSec = systemData.to.security.toFixed(1).toString(); + var tempSystemSec = systemSec; + + if(tempSystemSec < 0){ + tempSystemSec = '0-0'; + } + + systemSecClass += tempSystemSec.replace('.', '-'); + var system = ''; + jumpData.push( system ); + + }); + + + rowData.push( jumpData.join(' ') ); + + return rowData; + }; + + var getModule = function(systemData){ + + var moduleElement = null; + + // load trade routes for k-space systems + if(systemData.type.id === 2){ + + // create new module container + moduleElement = $('
    ', { + class: [config.moduleClass, config.systemRouteModuleClass].join(' ') + }); + + // headline + var headline = $('
    ', { + class: 'pull-left', + text: 'Routes' + }); + + moduleElement.append(headline); + + var systemFrom = systemData.name; + var systemsTo = ['Jita', 'Amarr', 'Rens', 'Dodixie']; + + // crate new route table + var table = $('
  • ', { + class: ['compact', 'stripe', 'order-column', 'row-border', config.systemInfoRoutesTableClass].join(' ') + }); + + moduleElement.append( $(table) ); + + // init empty table + var routesTable = table.DataTable( { + paging: false, + ordering: true, + order: [ 1, 'asc' ], + info: false, + searching: false, + hover: false, + + autoWidth: false, + columnDefs: [ + { + targets: 0, + orderable: true, + title: 'system   ' + },{ + targets: 1, + orderable: true, + title: 'jumps   ', + width: '40px', + class: 'text-right' + },{ + targets: 2, + orderable: false, + title: 'route' + } + ], + data: [] // will be added dynamic + }); + + // add toolbar buttons for table ------------------------------------- + var tableToolbar = $('
    ', { + class: [config.tableToolsClass, 'pull-right'].join(' ') + }).append( + $('
    {{#effectName}} - + - + {{/effectName}} {{#system.static}} @@ -48,12 +54,10 @@ {{/system.static}} - +
    Effect{{effectName}} {{effectName}}
    Security{{system.trueSec}}{{trueSec}}
    - -
    diff --git a/sass/layout/_images.scss b/sass/layout/_images.scss index 05af6f46..025c998b 100644 --- a/sass/layout/_images.scss +++ b/sass/layout/_images.scss @@ -3,9 +3,25 @@ } .pf-icon-dotlan{ - background: url('../img/icons/dotlan_logo.png'); + background: inline-image("#{$base-url}/icons/dotlan_logo.png"); width: 17px; height: 17px; opacity: 0.8; - margin: -5px 10px 0 10px; + margin: -5px 0px 0 10px; +} + +.pf-icon-wormhol-es{ + background: inline-image("#{$base-url}/icons/wormhol_es_logo.png"); + width: 17px; + height: 17px; + opacity: 0.8; + margin: -5px 0px 0 10px; +} + +.pf-icon-zkillboard{ + background: inline-image("#{$base-url}/icons/zkillboard_logo.png"); + width: 16px; + height: 16px; + opacity: 0.8; + margin: -5px 0px 0 10px; } diff --git a/sass/layout/_main.scss b/sass/layout/_main.scss index 7aff31f6..87f21899 100644 --- a/sass/layout/_main.scss +++ b/sass/layout/_main.scss @@ -27,6 +27,28 @@ em{ } } +.pf-font-capitalize{ + text-transform: capitalize; +} + +// table styles ===================================================== + +// table icon toolbar +.pf-table-tools{ + height: 45px; + + .btn:not(:last-child){ + margin-right: 10px; + } +} + +.pf-table-tools-action{ + height: 75px; + display: none; // triggered by js +} + + + // full screen view of an element /* .pf-fullscreen{ @@ -125,7 +147,7 @@ em{ } .pf-map-type-tab-global{ - border-top: 2px solid $green; + border-top: 2px solid $teal; } .pf-map-type-tab-alliance{ @@ -133,7 +155,11 @@ em{ } .pf-map-type-tab-private{ - border-top: 2px solid $teal; + border-top: 2px solid $green; + } + + .fa{ + margin-right: 5px; } } @@ -553,5 +579,9 @@ Animate the stripes } } +.row{ + will-change: all, transform; + +} diff --git a/sass/layout/_map.scss b/sass/layout/_map.scss index cc5fe179..0fa644d2 100644 --- a/sass/layout/_map.scss +++ b/sass/layout/_map.scss @@ -302,7 +302,7 @@ $mapWidth: 2500px; stroke-linecap: round; // line endings path{ - @include transition( stroke 0.2s ease-out); + @include transition( stroke 0.2s ease-out) ; } path:not(:first-child){ diff --git a/sass/layout/_system-info.scss b/sass/layout/_system-info.scss index e3dbf8e3..902f46d0 100644 --- a/sass/layout/_system-info.scss +++ b/sass/layout/_system-info.scss @@ -25,20 +25,6 @@ padding: 0 5px; } } - - // system info killboard graph =========================================== - .pf-system-info-graph-kills{ - width: 100%; - height: 150px; - padding: 10px 0; - position: relative; - } - - // routes table ========================================================== - .pf-system-route-table{ - font-size: 10px; - } - } @@ -104,23 +90,39 @@ } } } +} - // table icon toolbar - .pf-sig-table-tools{ - height: 45px; +// system graph module =================================================== +.pf-system-graph-module{ - .btn:not(:last-child){ - margin-right: 10px; - } + .pf-system-graph{ + width: 100%; + height: 100px; + } +} + +// route module ========================================================== +.pf-system-route-module{ + + .pf-system-route-table{ + width: 100%; + font-size: 10px; + } +} + +// killboard module ====================================================== +.pf-system-killboard-module{ + + .pf-system-killboard-graphs{ + margin-bottom: 10px; } - .pf-sig-table-tools-action{ - height: 75px; - display: none; // triggered by js + .pf-system-killboard-graph-kills{ + width: 100%; + height: 100px; + position: relative; } - } - diff --git a/sass/smartadmin/_main-colorpallet.scss b/sass/smartadmin/_main-colorpallet.scss index bef57495..dc1893ab 100644 --- a/sass/smartadmin/_main-colorpallet.scss +++ b/sass/smartadmin/_main-colorpallet.scss @@ -13,7 +13,9 @@ &.txt-color-green { color: $green !important; } &.txt-color-greenLight { color: $greenLight !important; } &.txt-color-greenDark { color: $greenDark !important; } + &.txt-color-redLight { color: $redLight !important; } &.txt-color-red { color: $red !important; } + &.txt-color-redDarker { color: $red-darker !important; } &.txt-color-yellow { color: $yellow !important; } &.txt-color-orange { color: $orange !important; } &.txt-color-orangeDark { color: $orangeDark !important; } @@ -25,7 +27,6 @@ &.txt-color-white { color: $white !important; } &.txt-color-magenta { color: $magenta !important; } &.txt-color-teal { color: $teal !important; } - &.txt-color-redLight { color: $redLight !important; } &.txt-color-primary { color: $brand-primary !important; } &.txt-color-success { color: $brand-success !important; } diff --git a/sass/smartadmin/_main.scss b/sass/smartadmin/_main.scss index aba49648..7f790678 100644 --- a/sass/smartadmin/_main.scss +++ b/sass/smartadmin/_main.scss @@ -163,7 +163,7 @@ h4 { h5 { font-size: $font-size-h5; font-weight: 300; - margin: 10px 0; + margin-top: 0; line-height:normal; }