diff --git a/app/cron.ini b/app/cron.ini index 19f16fe6..123aab6e 100644 --- a/app/cron.ini +++ b/app/cron.ini @@ -17,15 +17,18 @@ sixthHour = */10 * * * * halfHour = */30 * * * * [CRON.jobs] +; delete character log data +deleteLogData = Cron\CharacterUpdate->deleteLogData, @sixthHour + +; delete expired signatures +deleteSignatures = Cron\MapUpdate->deleteSignatures, @halfHour + ; import system data (jump, kill,..) from CCP API importSystemData = Cron\CcpSystemsUpdate->importSystemData, @hourly ; disable outdated maps deactivateMapData = Cron\MapUpdate->deactivateMapData, @hourly -; delete character log data -deleteLogData = Cron\CharacterUpdate->deleteLogData, @sixthHour - ; delete disabled maps deleteMapData = Cron\MapUpdate->deleteMapData, @downtime @@ -33,7 +36,7 @@ deleteMapData = Cron\MapUpdate->deleteMapData, @downtime deleteAuthenticationData = Cron\CharacterUpdate->deleteAuthenticationData, @downtime ; delete expired cache files -deleteExpiredCacheData = Cron\Cache->deleteExpiredData, @weekly +deleteExpiredCacheData = Cron\Cache->deleteExpiredData, @downtime -; delete expired signatures -deleteSignatures = Cron\MapUpdate->deleteSignatures, @halfHour \ No newline at end of file +; delete old statistics (activity log) data +deleteStatisticsData = Cron\StatisticsUpdate->deleteStatisticsData, @weekly \ No newline at end of file diff --git a/app/main/controller/api/connection.php b/app/main/controller/api/connection.php index 7bb7fe3d..b217e2bd 100644 --- a/app/main/controller/api/connection.php +++ b/app/main/controller/api/connection.php @@ -10,7 +10,7 @@ namespace Controller\Api; use Controller; use Model; -class Connection extends Controller\AccessController{ +class Connection extends Controller\AccessController { /** * @param \Base $f3 diff --git a/app/main/controller/api/map.php b/app/main/controller/api/map.php index 170e6051..08850a10 100644 --- a/app/main/controller/api/map.php +++ b/app/main/controller/api/map.php @@ -174,6 +174,14 @@ class Map extends Controller\AccessController { ]; $return->maxSharedCount = $maxSharedCount; + // get activity log options per map --------------------------------------------------------------------------- + $activityLogging = [ + 'character' => $f3->get('PATHFINDER.MAP.PRIVATE.ACTIVITY_LOGGING'), + 'corporation' => $f3->get('PATHFINDER.MAP.CORPORATION.ACTIVITY_LOGGING'), + 'alliance' => $f3->get('PATHFINDER.MAP.ALLIANCE.ACTIVITY_LOGGING'), + ]; + $return->activityLogging = $activityLogging; + // get program routes ----------------------------------------------------------------------------------------- $return->routes = [ 'ssoLogin' => $this->getF3()->alias( 'sso', ['action' => 'requestAuthorization'] ) @@ -223,11 +231,13 @@ class Map extends Controller\AccessController { * @var $system Model\SystemModel */ $system = Model\BasicModel::getNew('SystemModel'); + $system->setActivityLogging(false); /** * @var $connection Model\ConnectionModel */ $connection = Model\BasicModel::getNew('ConnectionModel'); + $connection->setActivityLogging(false); foreach($importData['mapData'] as $mapData){ if( diff --git a/app/main/controller/api/route.php b/app/main/controller/api/route.php index 7dbd5f09..2210c64c 100644 --- a/app/main/controller/api/route.php +++ b/app/main/controller/api/route.php @@ -7,6 +7,7 @@ */ namespace Controller\Api; +use Controller; use Model; @@ -15,7 +16,7 @@ use Model; * Class Route * @package Controller\Api */ -class Route extends \Controller\AccessController { +class Route extends Controller\AccessController { /** * cache time for static jump data (e.g. K-Space stargates) diff --git a/app/main/controller/api/signature.php b/app/main/controller/api/signature.php index 1f5e044b..03795fab 100644 --- a/app/main/controller/api/signature.php +++ b/app/main/controller/api/signature.php @@ -11,7 +11,7 @@ use Controller; use Model; -class Signature extends Controller\AccessController{ +class Signature extends Controller\AccessController { /** * event handler diff --git a/app/main/controller/api/statistic.php b/app/main/controller/api/statistic.php new file mode 100644 index 00000000..b2067732 --- /dev/null +++ b/app/main/controller/api/statistic.php @@ -0,0 +1,291 @@ + 10 will get prefixed with "0" + * -> e.g. year 2016, week 2 => "201602" + * @param int $year + * @param int $week + * @return string + */ + protected function concatYearWeek($year, $week){ + return strval($year) . str_pad($week, 2, 0, STR_PAD_LEFT); + } + + /** + * get max count of weeks in a year + * @param $year + * @return int + */ + protected function getIsoWeeksInYear($year) { + $date = new \DateTime; + $date->setISODate($year, 53); + return ($date->format('W') === '53' ? 53 : 52); + } + + /** + * get number of weeks for a given period + * @param string $period + * @param int $year + * @return int + */ + protected function getWeekCount($period, $year){ + $weeksInYear = $this->getIsoWeeksInYear($year); + + switch($period){ + case 'yearly': + $weekCount = $weeksInYear; + break; + case 'monthly': + $weekCount = 4; + break; + case 'weekly': + default: + $weekCount = 1; + break; + } + + return $weekCount; + } + + /** + * calculate calendar week and year for a given offset (weekCount) + * -> count forward OR backward + * @param int $year + * @param int $week + * @param int $weekCount + * @param bool $backwards + * @return array + */ + protected function calculateYearWeekOffset($year, $week, $weekCount, $backwards = false){ + $offset = [ + 'year' => (int)$year, + 'week' => (int)$week + ]; + + $weeksInYear = $this->getIsoWeeksInYear($year); + + // just for security... + if($offset['week'] > $weeksInYear){ + $offset['week'] = $weeksInYear; + }elseif($offset['week'] <= 0){ + $offset['week'] = 1; + } + + for($i = 1; $i < $weekCount; $i++){ + + if($backwards){ + // calculate backward + $offset['week']--; + + if($offset['week'] <= 0){ + // year change -> reset yearWeeks + $offset['year']--; + $offset['week'] = $this->getIsoWeeksInYear($offset['year']); + } + }else{ + // calculate forward + $offset['week']++; + + if($offset['week'] > $weeksInYear){ + // year change -> reset yearWeeks + $offset['week'] = 1; + $offset['year']++; + $weeksInYear = $this->getIsoWeeksInYear($offset['year']); + } + } + } + + return $offset; + } + + /** + * query statistic data for "activity log" + * -> group result by characterId + * @param CharacterModel $character + * @param int $typeId + * @param int $yearStart + * @param int $weekStart + * @param int $yearEnd + * @param int $weekEnd + * @return array + */ + protected function queryStatistic( CharacterModel $character, $typeId, $yearStart, $weekStart, $yearEnd, $weekEnd){ + $data = []; + + // can be either "characterId" || "corporationId" || "allianceId" + // -> is required (>0) to limit the result to only accessible data for the given character! + $objectId = 0; + + // add map-"typeId" (private/corp/ally) condition ------------------------------------------------------------- + // check if "ACTIVITY_LOGGING" is active for a given "typeId" + $sqlMapType = ""; + + switch($typeId){ + case 2: + if( $this->getF3()->get('PATHFINDER.MAP.PRIVATE.ACTIVITY_LOGGING') ){ + $sqlMapType .= " AND `character`.`id` = :objectId "; + $objectId = $character->_id; + } + break; + case 3: + if( + $this->getF3()->get('PATHFINDER.MAP.CORPORATION.ACTIVITY_LOGGING') && + $character->hasCorporation() + ){ + $sqlMapType .= " AND `character`.`corporationId` = :objectId "; + $objectId = $character->get('corporationId', true); + } + break; + case 4: + if( + $this->getF3()->get('PATHFINDER.MAP.ALLIANCE.ACTIVITY_LOGGING') && + $character->hasAlliance() + ){ + $sqlMapType .= " AND `character`.`allianceId` = :objectId "; + $objectId = $character->get('allianceId', true); + } + break; + } + + if($objectId > 0){ + + $queryData = [ + ':active' => 1, + ':objectId' => $objectId + ]; + + // date offset condition ---------------------------------------------------------------------------------- + $sqlDateOffset = " AND CONCAT(`log`.`year`, `log`.`week`) BETWEEN :yearWeekStart AND :yearWeekEnd "; + + $queryData[':yearWeekStart'] = $this->concatYearWeek($yearStart, $weekStart); + $queryData[':yearWeekEnd'] = $this->concatYearWeek($yearEnd, $weekEnd); + + // build query -------------------------------------------------------------------------------------------- + $sql = "SELECT + `log`.`year`, + `log`.`week`, + `log`.`characterId`, + `character`.`name`, + `character`.`lastLogin`, + SUM(`log`.`systemCreate`) `systemCreate`, + SUM(`log`.`systemUpdate`) `systemUpdate`, + SUM(`log`.`systemDelete`) `systemDelete`, + SUM(`log`.`connectionCreate`) `connectionCreate`, + SUM(`log`.`connectionUpdate`) `connectionUpdate`, + SUM(`log`.`connectionDelete`) `connectionDelete`, + SUM(`log`.`signatureCreate`) `signatureCreate`, + SUM(`log`.`signatureUpdate`) `signatureUpdate`, + SUM(`log`.`signatureDelete`) `signatureDelete` + FROM + `activity_log` `log` INNER JOIN + `character` ON + `character`.`id` = `log`.`characterId` + WHERE + `log`.`active` = :active + " . $sqlMapType . " + " . $sqlDateOffset . " + GROUP BY + `log`.`year`, + `log`.`week`, + `log`.`characterId` + ORDER BY + `log`.`year` DESC, `log`.`week` DESC"; + + $result = $this->getDB()->exec($sql, $queryData); + + if( !empty($result) ){ + // group result by characterId + foreach ($result as $key => &$entry) { + $tmp = $entry; + unset($tmp['characterId']); + unset($tmp['name']); + unset($tmp['lastLogin']); + $data[$entry['characterId']]['name'] = $entry['name']; + $data[$entry['characterId']]['lastLogin'] = strtotime($entry['lastLogin']); + $data[$entry['characterId']]['weeks'][ $entry['year'] . $entry['week'] ] = $tmp; + } + } + } + + return $data; + } + + /** + * get statistics data + * @param \Base $f3 + */ + public function getData(\Base $f3){ + $postData = (array)$f3->get('POST'); + $return = (object) []; + + $period = $postData['period']; + $typeId = (int)$postData['typeId']; + $yearStart = (int)$postData['year']; + $weekStart = (int)$postData['week']; + + $currentYear = (int)date('o'); + $currentWeek = (int)date('W'); + + if( + $yearStart && + $weekStart + ){ + $weekCount = $this->getWeekCount($period, $yearStart); + }else{ + // if start date is not set -> calculate it from current data + $tmpYear = $currentYear; + if($period == 'yearly'){ + $tmpYear--; + } + $weekCount = $this->getWeekCount($period, $tmpYear); + $offsetStart = $this->calculateYearWeekOffset($currentYear, $currentWeek, $weekCount, true); + $yearStart = $offsetStart['year']; + $weekStart = $offsetStart['week']; + } + + // date offset for statistics query + $offset = $this->calculateYearWeekOffset($yearStart, $weekStart, $weekCount); + + $activeCharacter = $this->getCharacter(); + + $return->statistics = $this->queryStatistic($activeCharacter, $typeId, $yearStart, $weekStart, $offset['year'], $offset['week']); + $return->period = $period; + $return->typeId = $typeId; + $return->weekCount = $weekCount; + $return->yearWeeks = [ + $yearStart => $this->getIsoWeeksInYear($yearStart), + ($yearStart + 1) => $this->getIsoWeeksInYear($yearStart + 1) + ]; + + // pagination offset + $offsetNext = $this->calculateYearWeekOffset($yearStart, $weekStart, $weekCount + 1); + $offsetPrev = $this->calculateYearWeekOffset($yearStart, $weekStart, $weekCount + 1, true); + + // check if "next" button is available (not in future) + $currentCurrentDataConcat = intval( $this->concatYearWeek($currentYear, $currentWeek) ); + $offsetNextDateConcat = intval( $this->concatYearWeek($offsetNext['year'], $offsetNext['week']) ); + if( $offsetNextDateConcat <= $currentCurrentDataConcat){ + $return->next = $offsetNext; + } + + $return->prev = $offsetPrev; + $return->start = ['year' => $yearStart, 'week' => $weekStart]; + $return->offset = $offset; + + echo json_encode($return); + } +} \ No newline at end of file diff --git a/app/main/controller/api/system.php b/app/main/controller/api/system.php index 1fd19579..73546f1e 100644 --- a/app/main/controller/api/system.php +++ b/app/main/controller/api/system.php @@ -7,11 +7,12 @@ */ namespace Controller\Api; +use Controller; use Controller\Ccp\Sso; use Data\Mapper as Mapper; use Model; -class System extends \Controller\AccessController { +class System extends Controller\AccessController { private $mainQuery = "SELECT map_sys.constellationID `connstallation_id`, @@ -68,7 +69,6 @@ class System extends \Controller\AccessController { * @param \Base $f3 */ function beforeroute(\Base $f3) { - parent::beforeroute($f3); // set header for all routes @@ -80,7 +80,6 @@ class System extends \Controller\AccessController { * @return string */ private function _getQuery(){ - $query = $this->mainQuery; $query .= ' ' . $this->whereQuery; $query .= ' ' . $this->havingQuery; @@ -419,8 +418,17 @@ class System extends \Controller\AccessController { foreach($systemIds as $systemId){ $system->getById($systemId); if( $system->hasAccess($activeCharacter) ){ - $system->setActive(false); - $system->save(); + // check whether system should be deleted OR set "inactive" + if( + empty($system->alias) && + empty($system->description) + ){ + $system->erase(); + }else{ + // keep data -> set "inactive" + $system->setActive(false); + $system->save(); + } $system->reset(); } } diff --git a/app/main/controller/controller.php b/app/main/controller/controller.php index d43aef46..c31b0069 100644 --- a/app/main/controller/controller.php +++ b/app/main/controller/controller.php @@ -95,6 +95,9 @@ class Controller { * @param \Base $f3 */ public function afterroute(\Base $f3){ + // store all user activities that are buffered for logging in this request + self::storeActivities(); + if($this->getTemplate()){ // Ajax calls don´t need a page render.. // this happens on client side @@ -779,6 +782,13 @@ class Controller { return LogController::getLogger($type); } + /** + * store activity log data to DB + */ + static function storeActivities(){ + LogController::instance()->storeActivities(); + } + /** * removes illegal characters from a Hive-key that are not allowed * @param $key diff --git a/app/main/controller/logcontroller.php b/app/main/controller/logcontroller.php index 3b919ad5..64bcbb11 100644 --- a/app/main/controller/logcontroller.php +++ b/app/main/controller/logcontroller.php @@ -7,10 +7,128 @@ */ namespace controller; +use DB; +class LogController extends \Prefab { -class LogController extends Controller { + /** + * buffered activity log data for this singleton LogController() class + * -> this buffered data can be stored somewhere (e.g. DB) before HTTP response + * -> should be cleared afterwards! + * @var array + */ + protected $activityLogBuffer = []; + /** + * reserve a "new" character activity for logging + * @param $characterId + * @param $mapId + * @param $action + */ + public function bufferActivity($characterId, $mapId, $action){ + $characterId = (int)$characterId; + $mapId = (int)$mapId; + + if( + $characterId > 0 && + $mapId > 0 + ){ + $key = $this->getBufferedActivityKey($characterId, $mapId); + + if( is_null($key) ){ + $activity = [ + 'characterId' => $characterId, + 'mapId' => $mapId, + $action => 1 + ]; + $this->activityLogBuffer[] = $activity; + }else{ + $this->activityLogBuffer[$key][$action]++; + } + } + } + + /** + * store all buffered activity log data to DB + */ + public function storeActivities(){ + if( !empty($this->activityLogBuffer) ){ + $db = DB\Database::instance()->getDB('PF'); + + $quoteStr = function($str) use ($db) { + return $db->quotekey($str); + }; + + $placeholderStr = function($str){ + return ':' . $str; + }; + + $updateRule = function($str){ + return $str . " = " . $str . " + VALUES(" . $str . ")"; + }; + + $year = (int)date('o'); + $yearWeek = (int)date('W'); + $db->begin(); + + foreach($this->activityLogBuffer as $activityData){ + $activityData['year'] = $year; + $activityData['week'] = $yearWeek; + + $columns = array_keys($activityData); + $columnsQuoted = array_map($quoteStr, $columns); + $placeholder = array_map($placeholderStr, $columns); + $args = array_combine($placeholder, $activityData); + + // "filter" columns that can be updated + $columnsForUpdate = array_diff($columns, ['year', 'week', 'characterId', 'mapId']); + $updateSql = array_map($updateRule, $columnsForUpdate); + + $sql = "INSERT DELAYED INTO + activity_log (" . implode(', ', $columnsQuoted) . ") values( + " . implode(', ', $placeholder) . " + ) + ON DUPLICATE KEY UPDATE + updated = NOW(), + " . implode(', ', $updateSql) . " + "; + + $db->exec($sql, $args); + } + + $db->commit(); + + // clear activity data for this instance + $this->activityLogBuffer = []; + } + } + + /** + * get array key from "buffered activity log" array + * @param int $characterId + * @param int $mapId + * @return int|null + */ + private function getBufferedActivityKey($characterId, $mapId){ + $activityKey = null; + + if( + $characterId > 0 && + $mapId > 0 + ){ + foreach($this->activityLogBuffer as $key => $activityData){ + if( + $activityData['characterId'] === $characterId && + $activityData['mapId'] === $mapId + ){ + $activityKey = $key; + break; + } + } + } + + return $activityKey; + } /** * get Logger instance diff --git a/app/main/controller/setup.php b/app/main/controller/setup.php index e4bcf080..b589aac8 100644 --- a/app/main/controller/setup.php +++ b/app/main/controller/setup.php @@ -86,6 +86,8 @@ class Setup extends Controller { 'Model\ConnectionModel', 'Model\SystemSignatureModel', + 'Model\ActivityLogModel', + 'Model\SystemShipKillModel', 'Model\SystemPodKillModel', 'Model\SystemFactionKillModel', diff --git a/app/main/cron/characterupdate.php b/app/main/cron/characterupdate.php index b34c33a7..042d9242 100644 --- a/app/main/cron/characterupdate.php +++ b/app/main/cron/characterupdate.php @@ -47,7 +47,7 @@ class CharacterUpdate { * delete expired character authentication data * authentication data is used for cookie based login * >> php index.php "/cron/deleteAuthenticationData" - * @param $f3 + * @param \Base $f3 */ function deleteAuthenticationData($f3){ DB\Database::instance()->getDB('PF'); diff --git a/app/main/cron/mapupdate.php b/app/main/cron/mapupdate.php index fe86fc88..6ca8912f 100644 --- a/app/main/cron/mapupdate.php +++ b/app/main/cron/mapupdate.php @@ -35,11 +35,6 @@ class MapUpdate { TIMESTAMPDIFF(DAY, map.updated, NOW() ) > :lifetime"; $pfDB->exec($sqlDeactivateExpiredMaps, ['lifetime' => $privateMapLifetime]); - $deactivatedMapsCount = $pfDB->count(); - - // Log ------------------------ - $log = new \Log('cron_' . __FUNCTION__ . '.log'); - $log->write( sprintf(self::LOG_TEXT_MAPS, __FUNCTION__, $deactivatedMapsCount) ); } } @@ -78,13 +73,13 @@ class MapUpdate { if($signatureExpire > 0){ $pfDB = DB\Database::instance()->getDB('PF'); - $sqlDeleteExpiredSignatures = "DELETE `sys` FROM - `system_signature` `sys` INNER JOIN + $sqlDeleteExpiredSignatures = "DELETE `sigs` FROM + `system_signature` `sigs` INNER JOIN `system` ON - `system`.`id` = `sys`.`systemId` + `system`.`id` = `sigs`.`systemId` WHERE `system`.`active` = 0 AND - TIMESTAMPDIFF(SECOND, `sys`.`updated`, NOW() ) > :lifetime + TIMESTAMPDIFF(SECOND, `sigs`.`updated`, NOW() ) > :lifetime "; $pfDB->exec($sqlDeleteExpiredSignatures, ['lifetime' => $signatureExpire]); diff --git a/app/main/cron/statisticsupdate.php b/app/main/cron/statisticsupdate.php new file mode 100644 index 00000000..26636709 --- /dev/null +++ b/app/main/cron/statisticsupdate.php @@ -0,0 +1,46 @@ + older than 1 year + * >> php index.php "/cron/deleteStatisticsData" + * @param \Base $f3 + */ + function deleteStatisticsData(\Base $f3){ + $currentYear = (int)date('o'); + $currentWeek = (int)date('W'); + $expiredYear = $currentYear - 1; + + $pfDB = DB\Database::instance()->getDB('PF'); + + $queryData = [ + 'yearWeekEnd' => strval($expiredYear) . str_pad($currentWeek, 2, 0, STR_PAD_LEFT) + ]; + + $sql = "DELETE FROM + activity_log + WHERE + CONCAT(`year`, `week`) < :yearWeekEnd"; + + $pfDB->exec($sql, $queryData); + + $deletedLogsCount = $pfDB->count(); + + // Log ------------------------ + $log = new \Log('cron_' . __FUNCTION__ . '.log'); + $log->write( sprintf(self::LOG_TEXT_STATISTICS, __FUNCTION__, $deletedLogsCount) ); + } +} \ No newline at end of file diff --git a/app/main/model/activitylogmodel.php b/app/main/model/activitylogmodel.php new file mode 100644 index 00000000..19a32b79 --- /dev/null +++ b/app/main/model/activitylogmodel.php @@ -0,0 +1,150 @@ + [ + 'type' => Schema::DT_BOOL, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + 'characterId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Model\CharacterModel', + 'constraint' => [ + [ + 'table' => 'character', + 'on-delete' => 'CASCADE' + ] + ] + ], + 'mapId' => [ + 'type' => Schema::DT_INT, + 'index' => true, + 'belongs-to-one' => 'Model\MapModel', + 'constraint' => [ + [ + 'table' => 'map', + 'on-delete' => 'SET NULL' // keep log data on map delete + ] + ] + ], + + // system actions ----------------------------------------------------- + + 'systemCreate' => [ + 'type' => Schema::DT_SMALLINT, + 'nullable' => false, + 'default' => 0, + ], + 'systemUpdate' => [ + 'type' => Schema::DT_SMALLINT, + 'nullable' => false, + 'default' => 0, + ], + 'systemDelete' => [ + 'type' => Schema::DT_SMALLINT, + 'nullable' => false, + 'default' => 0, + ], + + // connection actions ------------------------------------------------- + + 'connectionCreate' => [ + 'type' => Schema::DT_SMALLINT, + 'nullable' => false, + 'default' => 0, + ], + 'connectionUpdate' => [ + 'type' => Schema::DT_SMALLINT, + 'nullable' => false, + 'default' => 0, + ], + 'connectionDelete' => [ + 'type' => Schema::DT_SMALLINT, + 'nullable' => false, + 'default' => 0, + ], + + // signature actions ------------------------------------------------- + + 'signatureCreate' => [ + 'type' => Schema::DT_SMALLINT, + 'nullable' => false, + 'default' => 0, + ], + 'signatureUpdate' => [ + 'type' => Schema::DT_SMALLINT, + 'nullable' => false, + 'default' => 0, + ], + 'signatureDelete' => [ + 'type' => Schema::DT_SMALLINT, + 'nullable' => false, + 'default' => 0, + ], + ]; + + public function __construct($db = NULL, $table = NULL, $fluid = NULL, $ttl = 0){ + $this->addStaticDateFieldConfig(); + + parent::__construct($db, $table, $fluid, $ttl); + } + + /** + * extent the fieldConf Array with static fields for each table + */ + private function addStaticDateFieldConfig(){ + if(is_array($this->fieldConf)){ + $staticFieldConfig = [ + 'year' => [ + 'type' => Schema::DT_SMALLINT, + 'nullable' => false, + 'default' => date('o'), // 01.01 could be week 53 -> NOT "current" year! + 'index' => true + ], + 'week' => [ // week in year [1-53] + 'type' => Schema::DT_TINYINT, + 'nullable' => false, + 'default' => 1, + 'index' => true + ], + ]; + $this->fieldConf = array_merge($staticFieldConfig, $this->fieldConf); + } + } + + /** + * overwrites parent + * @param null $db + * @param null $table + * @param null $fields + * @return bool + */ + public static function setup($db=null, $table=null, $fields=null){ + $status = parent::setup($db,$table,$fields); + + if($status === true){ + $status = parent::setMultiColumnIndex(['year', 'week', 'characterId', 'mapId'], true); + if($status === true){ + $status = parent::setMultiColumnIndex(['year', 'week', 'characterId']); + } + } + + return $status; + } +} \ No newline at end of file diff --git a/app/main/model/basicmodel.php b/app/main/model/basicmodel.php index 69e270ab..959d0f98 100644 --- a/app/main/model/basicmodel.php +++ b/app/main/model/basicmodel.php @@ -56,6 +56,14 @@ abstract class BasicModel extends \DB\Cortex { */ protected $allowActiveChange = false; + /** + * enables check for $fieldChanges on update/insert + * -> fields that should be checked need an "activity-log" flag + * in $fieldConf config + * @var bool + */ + protected $enableActivityLogging = true; + /** * getData() cache key prefix * -> do not change, otherwise cached data is lost @@ -77,6 +85,12 @@ abstract class BasicModel extends \DB\Cortex { */ public static $enableDataImport = false; + /** + * changed fields (columns) on update/insert + * -> e.g. for character "activity logging" + * @var array + */ + protected $fieldChanges = []; public function __construct($db = NULL, $table = NULL, $fluid = NULL, $ttl = 0){ @@ -157,10 +171,54 @@ abstract class BasicModel extends \DB\Cortex { if(!$valid){ $this->throwValidationError($key); }else{ + $this->checkFieldForActivityLogging($key, $val); + return parent::set($key, $val); } } + /** + * change default "activity logging" status + * -> enable/disable + * @param $status + */ + public function setActivityLogging($status){ + $this->enableActivityLogging = (bool) $status; + } + + /** + * check column for value changes, + * --> if column is marked for "activity logging" + * @param string $key + * @param mixed $val + */ + protected function checkFieldForActivityLogging($key, $val){ + if( $this->enableActivityLogging ){ + $fieldConf = $this->fieldConf[$key]; + + // check for value changes if field has "activity logging" active + if($fieldConf['activity-log'] === true){ + if( + is_numeric($val) || + $fieldConf['type'] === Schema::DT_BOOL + ){ + $val = (int)$val; + } + + if( $fieldConf['type'] === self::DT_JSON){ + $currentValue = $this->get($key); + }else{ + $currentValue = $this->get($key, true); + } + + if($currentValue !== $val){ + // field has changed + in_array($key, $this->fieldChanges) ?: $this->fieldChanges[] = $key; + } + } + } + } + /** * setter for "active" status * -> default: keep current "active" status @@ -638,6 +696,18 @@ abstract class BasicModel extends \DB\Cortex { return ['added' => $addedCount, 'updated' => $updatedCount, 'deleted' => $deletedCount]; } + /** + * buffer a new activity (action) logging + * -> increment buffered counter + * -> log character activity create/update/delete events + * @param int $characterId + * @param int $mapId + * @param string $action + */ + protected function bufferActivity($characterId, $mapId, $action){ + Controller\LogController::instance()->bufferActivity($characterId, $mapId, $action); + } + /** * get the current class name * -> namespace not included diff --git a/app/main/model/characterlogmodel.php b/app/main/model/characterlogmodel.php index d54be720..2d9b8dc2 100644 --- a/app/main/model/characterlogmodel.php +++ b/app/main/model/characterlogmodel.php @@ -18,11 +18,11 @@ class CharacterLogModel extends BasicModel { /** * caching for relational data - * -> 10s matches REST API - Expire: Header-Data + * -> 5s matches REST API - Expire: Header-Data * for "Location" calls * @var int */ - protected $rel_ttl = 10; + protected $rel_ttl = 5; protected $fieldConf = [ 'active' => [ diff --git a/app/main/model/connectionmodel.php b/app/main/model/connectionmodel.php index 94a84f54..70a6abe6 100644 --- a/app/main/model/connectionmodel.php +++ b/app/main/model/connectionmodel.php @@ -8,8 +8,9 @@ namespace Model; -use Controller\Api\Route; use DB\SQL\Schema; +use Controller; +use Controller\Api\Route; class ConnectionModel extends BasicModel{ @@ -42,7 +43,8 @@ class ConnectionModel extends BasicModel{ 'table' => 'system', 'on-delete' => 'CASCADE' ] - ] + ], + 'activity-log' => true ], 'target' => [ 'type' => Schema::DT_INT, @@ -53,15 +55,18 @@ class ConnectionModel extends BasicModel{ 'table' => 'system', 'on-delete' => 'CASCADE' ] - ] + ], + 'activity-log' => true ], 'scope' => [ 'type' => Schema::DT_VARCHAR128, 'nullable' => false, - 'default' => '' + 'default' => '', + 'activity-log' => true ], 'type' => [ - 'type' => self::DT_JSON + 'type' => self::DT_JSON, + 'activity-log' => true ], 'eolUpdated' => [ 'type' => Schema::DT_TIMESTAMP, @@ -212,6 +217,66 @@ class ConnectionModel extends BasicModel{ return parent::beforeInsertEvent($self, $pkeys); } + /** + * Event "Hook" function + * return false will stop any further action + * @param self $self + * @param $pkeys + */ + public function afterInsertEvent($self, $pkeys){ + parent::afterInsertEvent($self, $pkeys); + + $self->logActivity('connectionCreate'); + } + + /** + * Event "Hook" function + * return false will stop any further action + * @param self $self + * @param $pkeys + */ + public function afterUpdateEvent($self, $pkeys){ + parent::afterUpdateEvent($self, $pkeys); + + $self->logActivity('connectionUpdate'); + } + + /** + * Event "Hook" function + * can be overwritten + * @param self $self + * @param $pkeys + */ + public function afterEraseEvent($self, $pkeys){ + parent::afterUpdateEvent($self, $pkeys); + + $self->logActivity('connectionDelete'); + } + + /** + * log character activity create/update/delete events + * @param string $action + */ + protected function logActivity($action){ + + if( + $this->enableActivityLogging && + ( + $action === 'connectionDelete' || + !empty($this->fieldChanges) + ) && + $this->get('mapId')->isActivityLogEnabled() + ){ + // TODO implement "dependency injection" for active character object... + $controller = new Controller\Controller(); + $currentActiveCharacter = $controller->getCharacter(); + $characterId = is_null($currentActiveCharacter) ? 0 : $currentActiveCharacter->_id; + $mapId = $this->get('mapId', true); + + parent::bufferActivity($characterId, $mapId, $action); + } + } + /** * save connection and check if obj is valid * @return ConnectionModel|false diff --git a/app/main/model/mapmodel.php b/app/main/model/mapmodel.php index 14293ff7..0293444f 100644 --- a/app/main/model/mapmodel.php +++ b/app/main/model/mapmodel.php @@ -609,6 +609,31 @@ class MapModel extends BasicModel { } } + /** + * check if "activity logging" is enabled for this map type + * @return bool + */ + public function isActivityLogEnabled(){ + $f3 = self::getF3(); + $activityLogEnabled = false; + + if( $this->isAlliance() ){ + if( $f3->get('PATHFINDER.MAP.ALLIANCE.ACTIVITY_LOGGING') ){ + $activityLogEnabled = true; + } + }elseif( $this->isCorporation() ){ + if( $f3->get('PATHFINDER.MAP.CORPORATION.ACTIVITY_LOGGING') ){ + $activityLogEnabled = true; + } + }elseif( $this->isPrivate() ){ + if( $f3->get('PATHFINDER.MAP.PRIVATE.ACTIVITY_LOGGING') ){ + $activityLogEnabled = true; + } + } + + return $activityLogEnabled; + } + /** * checks whether this map is private map * @return bool diff --git a/app/main/model/systemmodel.php b/app/main/model/systemmodel.php index f2b7cfa2..a5708fc6 100644 --- a/app/main/model/systemmodel.php +++ b/app/main/model/systemmodel.php @@ -24,7 +24,8 @@ class SystemModel extends BasicModel { 'type' => Schema::DT_BOOL, 'nullable' => false, 'default' => 1, - 'index' => true + 'index' => true, + 'activity-log' => true ], 'mapId' => [ 'type' => Schema::DT_INT, @@ -49,7 +50,8 @@ class SystemModel extends BasicModel { 'alias' => [ 'type' => Schema::DT_VARCHAR128, 'nullable' => false, - 'default' => '' + 'default' => '', + 'activity-log' => true ], 'regionId' => [ 'type' => Schema::DT_INT, @@ -106,12 +108,14 @@ class SystemModel extends BasicModel { 'table' => 'system_status', 'on-delete' => 'CASCADE' ] - ] + ], + 'activity-log' => true ], 'locked' => [ 'type' => Schema::DT_BOOL, 'nullable' => false, - 'default' => 0 + 'default' => 0, + 'activity-log' => true ], 'rallyUpdated' => [ 'type' => Schema::DT_TIMESTAMP, @@ -125,7 +129,8 @@ class SystemModel extends BasicModel { 'description' => [ 'type' => Schema::DT_VARCHAR512, 'nullable' => false, - 'default' => '' + 'default' => '', + 'activity-log' => true ], 'posX' => [ 'type' => Schema::DT_INT, @@ -351,6 +356,18 @@ class SystemModel extends BasicModel { return $rally; } + /** + * Event "Hook" function + * return false will stop any further action + * @param self $self + * @param $pkeys + */ + public function afterInsertEvent($self, $pkeys){ + parent::afterInsertEvent($self, $pkeys); + + $self->logActivity('systemCreate'); + } + /** * Event "Hook" function * can be overwritten @@ -360,6 +377,8 @@ class SystemModel extends BasicModel { * @return bool */ public function beforeUpdateEvent($self, $pkeys){ + $status = parent::beforeUpdateEvent($self, $pkeys); + if( !$self->isActive()){ // system becomes inactive // reset "rally point" fields @@ -372,7 +391,8 @@ class SystemModel extends BasicModel { $connection->erase(); } } - return true; + + return $status; } /** @@ -382,6 +402,8 @@ class SystemModel extends BasicModel { * @param $pkeys */ public function afterUpdateEvent($self, $pkeys){ + parent::afterUpdateEvent($self, $pkeys); + // check if rally point mail should be send if( $self->newRallyPointSet && @@ -389,6 +411,41 @@ class SystemModel extends BasicModel { ){ $self->sendRallyPointMail(); } + + $activity = ($self->isActive()) ? 'systemUpdate' : 'systemDelete'; + $self->logActivity($activity); + } + + /** + * Event "Hook" function + * can be overwritten + * @param self $self + * @param $pkeys + */ + public function afterEraseEvent($self, $pkeys){ + parent::afterUpdateEvent($self, $pkeys); + + $self->logActivity('systemDelete'); + } + + /** + * log character activity create/update/delete events + * @param string $action + */ + protected function logActivity($action){ + if( + $this->enableActivityLogging && + ( + $action === 'systemDelete' || + !empty($this->fieldChanges) + ) && + $this->get('mapId')->isActivityLogEnabled() + ){ + $characterId = $this->get('updatedCharacterId', true); + $mapId = $this->get('mapId', true); + + parent::bufferActivity($characterId, $mapId, $action); + } } /** diff --git a/app/main/model/systemsignaturemodel.php b/app/main/model/systemsignaturemodel.php index fb7583a0..be5baa0e 100644 --- a/app/main/model/systemsignaturemodel.php +++ b/app/main/model/systemsignaturemodel.php @@ -37,22 +37,26 @@ class SystemSignatureModel extends BasicModel { 'nullable' => false, 'default' => 0, 'index' => true, + 'activity-log' => true ], 'typeId' => [ 'type' => Schema::DT_INT, 'nullable' => false, 'default' => 0, 'index' => true, + 'activity-log' => true ], 'name' => [ 'type' => Schema::DT_VARCHAR128, 'nullable' => false, - 'default' => '' + 'default' => '', + 'activity-log' => true ], 'description' => [ 'type' => Schema::DT_VARCHAR512, 'nullable' => false, - 'default' => '' + 'default' => '', + 'activity-log' => true ], 'createdCharacterId' => [ 'type' => Schema::DT_INT, @@ -172,6 +176,68 @@ class SystemSignatureModel extends BasicModel { } } + /** + * Event "Hook" function + * return false will stop any further action + * @param self $self + * @param $pkeys + */ + public function afterInsertEvent($self, $pkeys){ + parent::afterInsertEvent($self, $pkeys); + + $self->logActivity('signatureCreate'); + } + + /** + * Event "Hook" function + * return false will stop any further action + * @param self $self + * @param $pkeys + */ + public function afterUpdateEvent($self, $pkeys){ + parent::afterUpdateEvent($self, $pkeys); + + $self->logActivity('signatureUpdate'); + } + + /** + * Event "Hook" function + * can be overwritten + * @param self $self + * @param $pkeys + */ + public function afterEraseEvent($self, $pkeys){ + parent::afterUpdateEvent($self, $pkeys); + + $self->logActivity('signatureDelete'); + } + + /** + * log character activity create/update/delete events + * @param string $action + */ + protected function logActivity($action){ + if($this->enableActivityLogging){ + /** + * @var $map MapModel + */ + $map = $this->get('systemId')->get('mapId'); + + if( + ( + $action === 'signatureDelete' || + !empty($this->fieldChanges) + ) && + $map->isActivityLogEnabled() + ){ + $characterId = $this->get('updatedCharacterId', true); + $mapId = $map->_id; + + parent::bufferActivity($characterId, $mapId, $action); + } + } + } + /** * overwrites parent * @param null $db diff --git a/app/pathfinder.ini b/app/pathfinder.ini index 24c354d4..aaa9ec7b 100644 --- a/app/pathfinder.ini +++ b/app/pathfinder.ini @@ -45,23 +45,32 @@ LOGIN = templates/view/login.html ; MAP ============================================================================================= ; Map settings for "private", "corporation" and "alliance" maps -; LIFETIME: Map will be deleted after "X" days, by cronjob -; MAX_COUNT: Users can create/view up to "X" maps of a type -; MAX_SHARED: Max number of shared entities per map +; LIFETIME (days): +; - Map will be deleted after "X" days, by cronjob +; MAX_COUNT: +; - Users can create/view up to "X" maps of a type +; MAX_SHARED: +; - Max number of shared entities per map +; ACTIVITY_LOGGING (0: disable, 1: enable): +; - Whether user activity should be logged for a map type +; - E.g. create/update/delete of systems/connections/signatures [PATHFINDER.MAP.PRIVATE] LIFETIME = 14 MAX_COUNT = 3 MAX_SHARED = 10 +ACTIVITY_LOGGING = 1 [PATHFINDER.MAP.CORPORATION] LIFETIME = 99999 MAX_COUNT = 3 MAX_SHARED = 3 +ACTIVITY_LOGGING = 1 [PATHFINDER.MAP.ALLIANCE] LIFETIME = 99999 MAX_COUNT = 3 MAX_SHARED = 2 +ACTIVITY_LOGGING = 0 ; Route search ==================================================================================== [PATHFINDER.ROUTE] @@ -108,8 +117,8 @@ EXECUTION_LIMIT = 50 CHARACTER_LOG = 300 ; expire time for static system data (seconds) (default: 20d) CONSTELLATION_SYSTEMS = 1728000 -; max expire time. Expired cache files will be deleted by cronjob (seconds) (default: 20d) -EXPIRE_MAX = 1728000 +; max expire time. Expired cache files will be deleted by cronjob (seconds) (default: 10d) +EXPIRE_MAX = 864000 ; expire time for signatures (inactive systems) (seconds) (default 3d) EXPIRE_SIGNATURES = 259200 diff --git a/js/app.js b/js/app.js index 9864489d..5e9fb456 100644 --- a/js/app.js +++ b/js/app.js @@ -38,6 +38,7 @@ requirejs.config({ raphael: 'lib/raphael-min', // v2.1.2 Raphaël - required for morris (dependency) bootbox: 'lib/bootbox.min', // v4.4.0 Bootbox.js - custom dialogs - http://bootboxjs.com easyPieChart: 'lib/jquery.easypiechart.min', // v2.1.6 Easy Pie Chart - HTML 5 pie charts - http://rendro.github.io/easy-pie-chart + peityInlineChart: 'lib/jquery.peity.min', // v3.2.0 Inline Chart - http://benpickles.github.io/peity/ dragToSelect: 'lib/jquery.dragToSelect', // v1.1 Drag to Select - http://andreaslagerkvist.com/jquery/drag-to-select hoverIntent: 'lib/jquery.hoverIntent.minified', // v1.8.0 Hover intention - http://cherne.net/brian/resources/jquery.hoverIntent.html fullScreen: 'lib/jquery.fullscreen.min', // v0.5.0 Full screen mode - https://github.com/private-face/jquery.fullscreen @@ -123,6 +124,9 @@ requirejs.config({ easyPieChart: { deps : ['jquery'] }, + peityInlineChart: { + deps : ['jquery'] + }, dragToSelect: { deps : ['jquery'] }, diff --git a/js/app/init.js b/js/app/init.js index fbbf142a..e99e6573 100644 --- a/js/app/init.js +++ b/js/app/init.js @@ -35,7 +35,6 @@ define(['jquery'], function($) { getSystemGraphData: 'api/system/graphData', // ajax URL - get all system graph data getConstellationData: 'api/system/constellationData', // ajax URL - get system constellation data setDestination: 'api/system/setDestination', // ajax URL - set destination - // connection API saveConnection: 'api/connection/save', // ajax URL - save new connection to map deleteConnection: 'api/connection/delete', // ajax URL - delete connection from map @@ -45,6 +44,8 @@ define(['jquery'], function($) { deleteSignatureData: 'api/signature/delete', // ajax URL - delete signature data for system // route API searchRoute: 'api/route/search', // ajax URL - search system routes + // stats API + getStatisticsData: 'api/statistic/getData', // ajax URL - get statistics data (activity log) // GitHub API gitHubReleases: 'api/github/releases' // ajax URL - get release info from GitHub }, @@ -52,6 +53,12 @@ define(['jquery'], function($) { ccpImageServer: 'https://image.eveonline.com/', // CCP image Server zKillboard: 'https://zkillboard.com/api/' // killboard api }, + breakpoints: [ + { name: 'desktop', width: Infinity }, + { name: 'tablet', width: 1200 }, + { name: 'fablet', width: 780 }, + { name: 'phone', width: 480 } + ], animationSpeed: { splashOverlay: 300, // "splash" loading overlay headerLink: 100, // links in head bar diff --git a/js/app/login.js b/js/app/login.js index 9a8c82ba..a38ba68a 100644 --- a/js/app/login.js +++ b/js/app/login.js @@ -105,6 +105,14 @@ define([ return ''; }; + /** + * set link observer for "version info" dialog + */ + var setVersionLinkObserver = function(){ + $('.' + config.navigationVersionLinkClass).off('click').on('click', function(e){ + $.fn.releasesDialog(); + }); + }; /** * set page observer @@ -121,11 +129,6 @@ define([ setCookie('cookie', 1, 365); }); - // releases ----------------------------------------------------------- - $('.' + config.navigationVersionLinkClass).on('click', function(e){ - $.fn.releasesDialog(); - }); - // manual ------------------------------------------------------------- $('.' + config.navigationLinkManualClass).on('click', function(e){ e.preventDefault(); @@ -138,6 +141,9 @@ define([ $.fn.showCreditsDialog(false, true); }); + // releases ----------------------------------------------------------- + setVersionLinkObserver(); + // tooltips ----------------------------------------------------------- var mapTooltipOptions = { toggle: 'tooltip', @@ -329,6 +335,9 @@ define([ } }; + /** + * init "YouTube" video preview + */ var initYoutube = function(){ $('.youtube').each(function() { @@ -468,6 +477,8 @@ define([ notificationPanel.velocity('transition.slideUpIn', { duration: 300, complete: function(){ + setVersionLinkObserver(); + // mark panel as "shown" Util.getLocalStorage().setItem(storageKey, currentVersion); } @@ -565,75 +576,71 @@ define([ } }); }; + // -------------------------------------------------------------------- - // request character data for each character panel - $('.' + config.characterSelectionClass + ' .pf-dynamic-area').each(function(){ - var characterElement = $(this); + requirejs(['text!templates/ui/character_panel.html', 'mustache'], function(template, Mustache){ - characterElement.showLoadingAnimation(); + $('.' + config.characterSelectionClass + ' .pf-dynamic-area').each(function(){ + var characterElement = $(this); - var requestData = { - cookie: characterElement.data('cookie') - }; + characterElement.showLoadingAnimation(); - $.ajax({ - type: 'POST', - url: Init.path.getCookieCharacterData, - data: requestData, - dataType: 'json', - context: { - href: characterElement.data('href'), - cookieName: requestData.cookie, - characterElement: characterElement - } - }).done(function(responseData, textStatus, request){ - var characterElement = this.characterElement; - characterElement.hideLoadingAnimation(); + var requestData = { + cookie: characterElement.data('cookie') + }; - if( - responseData.error && - responseData.error.length > 0 - ){ - $('.' + config.dynamicMessageContainerClass).showMessage({ - type: responseData.error[0].type, - title: 'Character verification failed', - text: responseData.error[0].message - }); - } + $.ajax({ + type: 'POST', + url: Init.path.getCookieCharacterData, + data: requestData, + dataType: 'json', + context: { + cookieName: requestData.cookie, + characterElement: characterElement + } + }).done(function(responseData, textStatus, request){ + this.characterElement.hideLoadingAnimation(); - if(responseData.hasOwnProperty('character')){ + if( + responseData.error && + responseData.error.length > 0 + ){ + $('.' + config.dynamicMessageContainerClass).showMessage({ + type: responseData.error[0].type, + title: 'Character verification failed', + text: responseData.error[0].message + }); + } - var data = { - link: this.href, - cookieName: this.cookieName, - character: responseData.character - }; + if(responseData.hasOwnProperty('character')){ + + var data = { + link: this.characterElement.data('href'), + cookieName: this.cookieName, + character: responseData.character + }; - requirejs(['text!templates/ui/character_panel.html', 'mustache'], function(template, Mustache) { var content = Mustache.render(template, data); - characterElement.html(content); + this.characterElement.html(content); // show character panel (animation settings) - initCharacterAnimation(characterElement.find('.' + config.characterImageWrapperClass)); - }); - }else{ + initCharacterAnimation(this.characterElement.find('.' + config.characterImageWrapperClass)); + }else{ + // character data not available -> remove panel + removeCharacterPanel(this.characterElement); + } + }).fail(function( jqXHR, status, error) { + var characterElement = this.characterElement; + characterElement.hideLoadingAnimation(); + // character data not available -> remove panel removeCharacterPanel(this.characterElement); - } - - }).fail(function( jqXHR, status, error) { - var characterElement = this.characterElement; - characterElement.hideLoadingAnimation(); - - // character data not available -> remove panel - removeCharacterPanel(this.characterElement); + }); }); - }); }; - /** * default ajax error handler * -> show user notifications diff --git a/js/app/map/map.js b/js/app/map/map.js index 78360627..09ec0875 100644 --- a/js/app/map/map.js +++ b/js/app/map/map.js @@ -1415,7 +1415,7 @@ define([ {subIcon: '', subAction: 'filter_jumpbridge', subText: 'jumpbridge'} ]}, {divider: true, action: 'delete_systems'}, - {icon: 'fa-eraser', action: 'delete_systems', text: 'delete systems'} + {icon: 'fa-trash', action: 'delete_systems', text: 'delete systems'} ] }; @@ -1452,7 +1452,7 @@ define([ ]}, {divider: true, action: 'delete_connection'}, - {icon: 'fa-eraser', action: 'delete_connection', text: 'delete'} + {icon: 'fa-trash', action: 'delete_connection', text: 'delete'} ] }; @@ -1495,7 +1495,7 @@ define([ {subIcon: 'fa-step-forward', subAction: 'add_last_waypoint', subText: 'add new [end]'} ]}, {divider: true, action: 'delete_system'}, - {icon: 'fa-eraser', action: 'delete_system', text: 'delete system(s)'} + {icon: 'fa-trash', action: 'delete_system', text: 'delete system(s)'} ] }; diff --git a/js/app/mappage.js b/js/app/mappage.js index 1d69719b..6cc7c4f3 100644 --- a/js/app/mappage.js +++ b/js/app/mappage.js @@ -62,6 +62,7 @@ define([ Init.maxSharedCount = initData.maxSharedCount; Init.routes = initData.routes; Init.notificationStatus = initData.notificationStatus; + Init.activityLogging = initData.activityLogging; // init tab change observer, Once the timers are available Page.initTabChangeObserver(); diff --git a/js/app/page.js b/js/app/page.js index 2e3d46d7..ed35e13e 100644 --- a/js/app/page.js +++ b/js/app/page.js @@ -12,6 +12,7 @@ define([ 'text!templates/modules/header.html', 'text!templates/modules/footer.html', 'dialog/notification', + 'dialog/stats', 'dialog/map_info', 'dialog/account_settings', 'dialog/manual', @@ -115,6 +116,22 @@ define([ setDocumentObserver(); }; + /** + * get main menu title element + * @param title + * @returns {JQuery|*|jQuery} + */ + var getMenuHeadline = function(title){ + return $('
';
+ }
+ }
+ },{
+ targets: 2,
+ title: 'name',
+ width: 200,
+ data: 'character',
+ render: {
+ _: 'name',
+ sort: 'name'
+ }
+ },{
+ targets: 3,
+ title: 'last login',
+ searchable: false,
+ width: 70,
+ className: ['text-right', 'separator-right'].join(' '),
+ data: 'character',
+ render: {
+ _: 'lastLogin',
+ sort: 'lastLogin'
+ },
+ createdCell: function(cell, cellData, rowData, rowIndex, colIndex){
+ $(cell).initTimestampCounter();
+ }
+ },{
+ targets: 4,
+ title: 'C ',
+ orderable: false,
+ searchable: false,
+ width: columnNumberWidth,
+ className: ['text-right', 'hidden-xs', 'hidden-sm'].join(' '),
+ data: 'systemCreate',
+ render: {
+ _: renderInlineChartColumn
+ }
+ },{
+ targets: 5,
+ title: 'U ',
+ orderable: false,
+ searchable: false,
+ width: columnNumberWidth,
+ className: ['text-right', 'hidden-xs', 'hidden-sm'].join(' '),
+ data: 'systemUpdate',
+ render: {
+ _: renderInlineChartColumn
+ }
+ },{
+ targets: 6,
+ title: 'D ',
+ orderable: false,
+ searchable: false,
+ width: columnNumberWidth,
+ className: ['text-right', 'hidden-xs', 'hidden-sm'].join(' '),
+ data: 'systemDelete',
+ render: {
+ _: renderInlineChartColumn
+ }
+ },{
+ targets: 7,
+ title: 'Σ ',
+ searchable: false,
+ width: 20,
+ className: ['text-right', 'hidden-xs', 'hidden-sm', 'separator-right'].join(' '),
+ data: 'systemSum',
+ render: {
+ _: renderNumericColumn
+ }
+ },{
+ targets: 8,
+ title: 'C ',
+ orderable: false,
+ searchable: false,
+ width: columnNumberWidth,
+ className: ['text-right', 'hidden-xs', 'hidden-sm'].join(' '),
+ data: 'connectionCreate',
+ render: {
+ _: renderInlineChartColumn
+ }
+ },{
+ targets: 9,
+ title: 'U ',
+ orderable: false,
+ searchable: false,
+ width: columnNumberWidth,
+ className: ['text-right', 'hidden-xs', 'hidden-sm'].join(' '),
+ data: 'connectionUpdate',
+ render: {
+ _: renderInlineChartColumn
+ }
+ },{
+ targets: 10,
+ title: 'D ',
+ orderable: false,
+ searchable: false,
+ width: columnNumberWidth,
+ className: ['text-right', 'hidden-xs', 'hidden-sm'].join(' '),
+ data: 'connectionDelete',
+ render: {
+ _: renderInlineChartColumn
+ }
+ },{
+ targets: 11,
+ title: 'Σ ',
+ searchable: false,
+ width: 20,
+ className: ['text-right', 'hidden-xs', 'hidden-sm', 'separator-right'].join(' '),
+ data: 'connectionSum',
+ render: {
+ _: renderNumericColumn
+ }
+ },{
+ targets: 12,
+ title: 'C ',
+ orderable: false,
+ searchable: false,
+ width: columnNumberWidth,
+ className: ['text-right', 'hidden-xs', 'hidden-sm'].join(' '),
+ data: 'signatureCreate',
+ render: {
+ _: renderInlineChartColumn
+ }
+ },{
+ targets: 13,
+ title: 'U ',
+ orderable: false,
+ searchable: false,
+ width: columnNumberWidth,
+ className: ['text-right', 'hidden-xs', 'hidden-sm'].join(' '),
+ data: 'signatureUpdate',
+ render: {
+ _: renderInlineChartColumn
+ }
+ },{
+ targets: 14,
+ title: 'D ',
+ orderable: false,
+ searchable: false,
+ width: columnNumberWidth,
+ className: ['text-right', 'hidden-xs', 'hidden-sm'].join(' '),
+ data: 'signatureDelete',
+ render: {
+ _: renderInlineChartColumn
+ }
+ },{
+ targets: 15,
+ title: 'Σ ',
+ searchable: false,
+ width: 20,
+ className: ['text-right', 'hidden-xs', 'hidden-sm', 'separator-right'].join(' '),
+ data: 'signatureSum',
+ render: {
+ _: renderNumericColumn
+ }
+ },{
+ targets: 16,
+ title: 'Σ ',
+ searchable: false,
+ width: 20,
+ className: 'text-right',
+ data: 'totalSum',
+ render: {
+ _: renderNumericColumn
+ }
+ }
+ ],
+ initComplete: function(settings){
+ var tableApi = this.api();
+
+ // initial statistics data request
+ var requestData = getRequestDataFromTabPanels(dialogElement);
+ getStatsData(requestData, {tableApi: tableApi, callback: drawStatsTable});
+ },
+ drawCallback: function(settings){
+ this.api().rows().nodes().to$().each(function(i, row){
+ $(row).find('.' + config.statsLineChartClass).peity('line', {
+ fill: 'transparent',
+ height: 18,
+ min: 0,
+ width: 50
+ });
+ });
+ },
+ footerCallback: function ( row, data, start, end, display ) {
+ var api = this.api();
+ var sumColumnIndexes = [7, 11, 15, 16];
+
+ // column data for "sum" columns over this page
+ var pageTotalColumns = api
+ .columns( sumColumnIndexes, { page: 'current'} )
+ .data();
+
+ // sum columns for "total" sum
+ pageTotalColumns.each(function(colData, index){
+ pageTotalColumns[index] = colData.reduce(function(a, b){
+ return a + b;
+ }, 0);
+ });
+
+ $(sumColumnIndexes).each(function(index, value){
+ $( api.column( value ).footer() ).text( renderNumericColumn(pageTotalColumns[index]) );
+ });
+ },
+ data: [] // will be added dynamic
+ });
+
+ statsTable.on('order.dt search.dt', function(){
+ statsTable.column(0, {search:'applied', order:'applied'}).nodes().each(function(cell, i){
+ $(cell).html( (i + 1) + '. ');
+ });
+ }).draw();
+
+ var tooltipElements = dialogElement.find('[data-toggle="tooltip"]');
+ tooltipElements.tooltip();
+ };
+
+ /**
+ * request raw statistics data and execute callback
+ * @param requestData
+ * @param context
+ */
+ var getStatsData = function(requestData, context){
+
+ context.dynamicArea = $('#' + config.statsContainerId + ' .pf-dynamic-area');
+ context.dynamicArea.showLoadingAnimation();
+
+ $.ajax({
+ type: 'POST',
+ url: Init.path.getStatisticsData,
+ data: requestData,
+ dataType: 'json',
+ context: context
+ }).done(function(data){
+ this.dynamicArea.hideLoadingAnimation();
+
+ this.callback(data);
+ }).fail(function( jqXHR, status, error) {
+ var reason = status + ' ' + error;
+ Util.showNotify({title: jqXHR.status + ': loadStatistics', text: reason, type: 'warning'});
+ });
+ };
+
+ /**
+ * update dataTable with response data
+ * update "header"/"filter" elements in dialog
+ * @param responseData
+ */
+ var drawStatsTable = function(responseData){
+ var dialogElement = $('#' + config.statsDialogId);
+
+ // update filter/header -----------------------------------------------------------------------------
+ var navigationListElements = $('.' + config.dialogNavigationClass);
+ navigationListElements.find('a[data-type="typeId"][data-value="' + responseData.typeId + '"]').tab('show');
+ navigationListElements.find('a[data-type="period"][data-value="' + responseData.period + '"]').tab('show');
+
+ // update period pagination -------------------------------------------------------------------------
+ var prevButton = dialogElement.find('.' + config.dialogNavigationPrevClass);
+ prevButton.data('newOffset', responseData.prev);
+ prevButton.find('span').text('Week ' + responseData.prev.week + ', ' + responseData.prev.year);
+ prevButton.css('visibility', 'visible');
+
+ var nextButton = dialogElement.find('.' + config.dialogNavigationNextClass);
+ if(responseData.next){
+ nextButton.data('newOffset', responseData.next);
+ nextButton.find('span').text('Week ' + responseData.next.week + ', ' + responseData.next.year);
+ nextButton.css('visibility', 'visible');
+ }else{
+ nextButton.css('visibility', 'hidden');
+ }
+
+ // update current period information label ----------------------------------------------------------
+ // if period == "weekly" there is no "offset" -> just a single week
+ var offsetText = 'Week ' + responseData.start.week + ', ' + responseData.start.year;
+ if(responseData.period !== 'weekly'){
+ offsetText += ' ' +
+ 'Week ' + responseData.offset.week + ', ' + responseData.offset.year;
+ }
+ dialogElement.find('.' + config.dialogNavigationOffsetClass)
+ .data('start', responseData.start)
+ .data('period', responseData.period)
+ .html(offsetText);
+
+ // clear and (re)-fill table ------------------------------------------------------------------------
+ var formattedData = formatStatisticsData(responseData);
+ this.tableApi.clear();
+ this.tableApi.rows.add(formattedData).draw();
+ };
+
+ /**
+ * format statistics data for dataTable
+ * -> e.g. format inline-chart data
+ * @param statsData
+ * @returns {Array}
+ */
+ var formatStatisticsData = function(statsData){
+ var formattedData = [];
+ var yearStart = statsData.start.year;
+ var weekStart = statsData.start.week;
+ var weekCount = statsData.weekCount;
+ var yearWeeks = statsData.yearWeeks;
+
+ var tempRand = function(min, max){
+ return Math.random() * (max - min) + min;
+ };
+
+ // format/sum week statistics data for inline charts
+ var formatWeekData = function(weeksData){
+ var currentYear = yearStart;
+ var currentWeek = weekStart;
+
+ var formattedWeeksData = {
+ systemCreate: [],
+ systemUpdate: [],
+ systemDelete: [],
+ connectionCreate: [],
+ connectionUpdate: [],
+ connectionDelete: [],
+ signatureCreate: [],
+ signatureUpdate: [],
+ signatureDelete: [],
+ systemSum: 0,
+ connectionSum: 0,
+ signatureSum: 0
+ };
+
+ for(let i = 0; i < weekCount; i++){
+ let yearWeekProp = currentYear + '' + currentWeek;
+
+ if(weeksData.hasOwnProperty( yearWeekProp )){
+ let weekData = weeksData[ yearWeekProp ];
+
+ // system -------------------------------------------------------------------------------
+ formattedWeeksData.systemCreate.push( weekData.systemCreate );
+ formattedWeeksData.systemSum += parseInt( weekData.systemCreate );
+
+ formattedWeeksData.systemUpdate.push( weekData.systemUpdate );
+ formattedWeeksData.systemSum += parseInt( weekData.systemUpdate );
+
+ formattedWeeksData.systemDelete.push( weekData.systemDelete );
+ formattedWeeksData.systemSum += parseInt( weekData.systemDelete );
+
+ // connection ---------------------------------------------------------------------------
+ formattedWeeksData.connectionCreate.push( weekData.connectionCreate );
+ formattedWeeksData.connectionSum += parseInt( weekData.connectionCreate );
+
+ formattedWeeksData.connectionUpdate.push( weekData.connectionUpdate );
+ formattedWeeksData.connectionSum += parseInt( weekData.connectionUpdate );
+
+ formattedWeeksData.connectionDelete.push( weekData.connectionDelete );
+ formattedWeeksData.connectionSum += parseInt( weekData.connectionDelete );
+
+ // signature ----------------------------------------------------------------------------
+ formattedWeeksData.signatureCreate.push( weekData.signatureCreate );
+ formattedWeeksData.signatureSum += parseInt( weekData.signatureCreate );
+
+ formattedWeeksData.signatureUpdate.push( weekData.signatureUpdate );
+ formattedWeeksData.signatureSum += parseInt( weekData.signatureUpdate );
+
+ formattedWeeksData.signatureDelete.push( weekData.signatureDelete );
+ formattedWeeksData.signatureSum += parseInt( weekData.signatureDelete );
+ }else{
+ // system -------------------------------------------------------------------------------
+ formattedWeeksData.systemCreate.push(0);
+ formattedWeeksData.systemUpdate.push(0);
+ formattedWeeksData.systemDelete.push(0);
+
+ // connection ---------------------------------------------------------------------------
+ formattedWeeksData.connectionCreate.push(0);
+ formattedWeeksData.connectionUpdate.push(0);
+ formattedWeeksData.connectionDelete.push(0);
+
+ // signature ----------------------------------------------------------------------------
+ formattedWeeksData.signatureCreate.push(0);
+ formattedWeeksData.signatureUpdate.push(0);
+ formattedWeeksData.signatureDelete.push(0);
+ }
+
+ currentWeek++;
+
+ if( currentWeek > yearWeeks[currentYear] ){
+ currentWeek = 1;
+ currentYear++;
+ }
+ }
+
+ // system ---------------------------------------------------------------------------------------
+ formattedWeeksData.systemCreate = formattedWeeksData.systemCreate.join(',');
+ formattedWeeksData.systemUpdate = formattedWeeksData.systemUpdate.join(',');
+ formattedWeeksData.systemDelete = formattedWeeksData.systemDelete.join(',');
+
+ // connection -----------------------------------------------------------------------------------
+ formattedWeeksData.connectionCreate = formattedWeeksData.connectionCreate.join(',');
+ formattedWeeksData.connectionUpdate = formattedWeeksData.connectionUpdate.join(',');
+ formattedWeeksData.connectionDelete = formattedWeeksData.connectionDelete.join(',');
+
+ // signature ------------------------------------------------------------------------------------
+ formattedWeeksData.signatureCreate = formattedWeeksData.signatureCreate.join(',');
+ formattedWeeksData.signatureUpdate = formattedWeeksData.signatureUpdate.join(',');
+ formattedWeeksData.signatureDelete = formattedWeeksData.signatureDelete.join(',');
+
+ return formattedWeeksData;
+ };
+
+ $.each(statsData.statistics, function(characterId, data){
+
+ var formattedWeeksData = formatWeekData(data.weeks);
+
+ var rowData = {
+ character: {
+ id: characterId,
+ name: data.name,
+ lastLogin: data.lastLogin
+ },
+ systemCreate: {
+ type: 'C',
+ data: formattedWeeksData.systemCreate
+ },
+ systemUpdate: {
+ type: 'U',
+ data: formattedWeeksData.systemUpdate
+ },
+ systemDelete: {
+ type: 'D',
+ data: formattedWeeksData.systemDelete
+ },
+ systemSum: formattedWeeksData.systemSum,
+ connectionCreate: {
+ type: 'C',
+ data: formattedWeeksData.connectionCreate
+ },
+ connectionUpdate: {
+ type: 'U',
+ data: formattedWeeksData.connectionUpdate
+ },
+ connectionDelete: {
+ type: 'D',
+ data: formattedWeeksData.connectionDelete
+ },
+ connectionSum: formattedWeeksData.connectionSum,
+ signatureCreate: {
+ type: 'C',
+ data: formattedWeeksData.signatureCreate
+ },
+ signatureUpdate: {
+ type: 'U',
+ data: formattedWeeksData.signatureUpdate
+ },
+ signatureDelete: {
+ type: 'D',
+ data: formattedWeeksData.signatureDelete
+ },
+ signatureSum: formattedWeeksData.signatureSum,
+ totalSum: formattedWeeksData.systemSum + formattedWeeksData.connectionSum + formattedWeeksData.signatureSum
+ };
+
+ formattedData.push(rowData);
+ });
+
+ return formattedData;
+ };
+
+ /**
+ *
+ * @param dialogElement
+ * @returns {{}}
+ */
+ var getRequestDataFromTabPanels = function(dialogElement){
+ var requestData = {};
+
+ // get data from "tab" panel links ------------------------------------------------------------------
+ var navigationListElements = dialogElement.find('.' + config.dialogNavigationClass);
+ navigationListElements.find('.' + config.dialogNavigationListItemClass + '.active a').each(function(){
+ var linkElement = $(this);
+ requestData[linkElement.data('type')]= linkElement.data('value');
+ });
+
+ // get current period (no offset) data (if available) -----------------------------------------------
+ var navigationOffsetElement = dialogElement.find('.' + config.dialogNavigationOffsetClass);
+ var startData = navigationOffsetElement.data('start');
+ var periodOld = navigationOffsetElement.data('period');
+
+ // if period switch was detected
+ // -> "year" and "week" should not be send
+ // -> start from "now"
+ if(
+ requestData.period === periodOld &&
+ startData
+ ){
+ requestData.year = startData.year;
+ requestData.week = startData.week;
+ }
+
+ return requestData;
+ };
+
+ /**
+ * check if "activity log" type is enabled for a group
+ * @param type
+ * @returns {boolean}
+ */
+ var isTabTypeEnabled = function(type){
+ var enabled = false;
+
+ switch(type){
+ case 'private':
+ if(Init.activityLogging.character){
+ enabled = true;
+ }
+ break;
+ case 'corporation':
+ if(
+ Init.activityLogging.corporation &&
+ Util.getCurrentUserInfo('corporationId')
+ ){
+ enabled = true;
+ }
+ break;
+ case 'alliance':
+ if(
+ Init.activityLogging.alliance &&
+ Util.getCurrentUserInfo('allianceId')
+ ){
+ enabled = true;
+ }
+ break;
+ }
+
+ return enabled;
+ };
+
+ /**
+ * show activity stats dialog
+ */
+ $.fn.showStatsDialog = function(){
+ requirejs(['text!templates/dialog/stats.html', 'mustache', 'peityInlineChart'], function(template, Mustache) {
+
+ var data = {
+ id: config.statsDialogId,
+ dialogNavigationClass: config.dialogNavigationClass,
+ dialogNavLiClass: config.dialogNavigationListItemClass,
+ enablePrivateTab: isTabTypeEnabled('private'),
+ enableCorporationTab: isTabTypeEnabled('corporation'),
+ enableAllianceTab: isTabTypeEnabled('alliance'),
+ statsContainerId: config.statsContainerId,
+ statsTableId: config.statsTableId,
+ dialogNavigationOffsetClass: config.dialogNavigationOffsetClass,
+ dialogNavigationPrevClass: config.dialogNavigationPrevClass,
+ dialogNavigationNextClass: config.dialogNavigationNextClass
+ };
+
+ var content = Mustache.render(template, data);
+
+ var statsDialog = bootbox.dialog({
+ title: 'Statistics',
+ message: content,
+ size: 'large',
+ show: false,
+ buttons: {
+ close: {
+ label: 'close',
+ className: 'btn-default'
+ }
+ }
+ });
+
+ // model events
+ statsDialog.on('show.bs.modal', function(e) {
+ var dialogElement = $(e.target);
+
+ initStatsTable(dialogElement);
+ });
+
+ // Tab module events
+ statsDialog.find('a[data-toggle="tab"]').on('show.bs.tab', function (e, b, c) {
+ if( $(e.target).parent().hasClass('disabled') ){
+ // no action on "disabled" tabs
+ return false;
+ }
+ });
+
+ statsDialog.find('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
+ var requestData = getRequestDataFromTabPanels(statsDialog);
+ var tableApi = statsDialog.find('#' + config.statsTableId).DataTable();
+
+ getStatsData(requestData, {tableApi: tableApi, callback: drawStatsTable});
+ });
+
+ // offset change links
+ statsDialog.find('.' + config.dialogNavigationPrevClass + ', .' + config.dialogNavigationNextClass).on('click', function(){
+ var offsetData = $(this).data('newOffset');
+ if(offsetData){
+ // this should NEVER fail!
+ // get "base" request data (e.g. typeId, period)
+ // --> overwrite period data with new period data
+ var tmpRequestData = getRequestDataFromTabPanels(statsDialog);
+ var requestData = $.extend({}, tmpRequestData, offsetData);
+ var tableApi = statsDialog.find('#' + config.statsTableId).DataTable();
+
+ getStatsData(requestData, {tableApi: tableApi, callback: drawStatsTable});
+ }
+ });
+
+ // show dialog
+ statsDialog.modal('show');
+ });
+ };
+});
\ No newline at end of file
diff --git a/js/app/ui/system_route.js b/js/app/ui/system_route.js
index 986ca8bd..08098284 100644
--- a/js/app/ui/system_route.js
+++ b/js/app/ui/system_route.js
@@ -316,7 +316,7 @@ define([
var settingsDialog = bootbox.dialog({
title: 'Route settings',
message: content,
- show: false,
+ show: false,
buttons: {
close: {
label: 'cancel',
@@ -444,7 +444,6 @@ define([
data: requestData,
context: context
}).done(function(routesData){
-
this.moduleElement.hideLoadingAnimation();
// execute callback
@@ -886,7 +885,14 @@ define([
};
-
+ /**
+ * draw route table
+ * @param mapId
+ * @param moduleElement
+ * @param systemFromData
+ * @param routesTable
+ * @param systemsTo
+ */
var drawRouteTable = function(mapId, moduleElement, systemFromData, routesTable, systemsTo){
var requestRouteData = [];
var currentTimestamp = Util.getServerTime().getTime();
diff --git a/js/app/util.js b/js/app/util.js
index 24244b14..8b4ef7b3 100644
--- a/js/app/util.js
+++ b/js/app/util.js
@@ -121,7 +121,9 @@ define([
var loadingElement = $(this);
var overlay = loadingElement.find('.' + config.ajaxOverlayClass );
- $(overlay).velocity('reverse', {
+ // important: "stop" is required to stop "show" animation
+ // -> otherwise "complete" callback is not fired!
+ $(overlay).velocity('stop').velocity('reverse', {
complete: function(){
$(this).remove();
// enable all events
diff --git a/js/lib/jquery.peity.min.js b/js/lib/jquery.peity.min.js
new file mode 100644
index 00000000..3d0166ee
--- /dev/null
+++ b/js/lib/jquery.peity.min.js
@@ -0,0 +1,13 @@
+// Peity jQuery plugin version 3.2.0
+// (c) 2015 Ben Pickles
+//
+// http://benpickles.github.io/peity
+//
+// Released under MIT license.
+(function(k,w,h,v){var d=k.fn.peity=function(a,b){y&&this.each(function(){var e=k(this),c=e.data("_peity");c?(a&&(c.type=a),k.extend(c.opts,b)):(c=new x(e,a,k.extend({},d.defaults[a],e.data("peity"),b)),e.change(function(){c.draw()}).data("_peity",c));c.draw()});return this},x=function(a,b,e){this.$el=a;this.type=b;this.opts=e},o=x.prototype,q=o.svgElement=function(a,b){return k(w.createElementNS("http://www.w3.org/2000/svg",a)).attr(b)},y="createElementNS"in w&&q("svg",{})[0].createSVGRect;o.draw=
+function(){var a=this.opts;d.graphers[this.type].call(this,a);a.after&&a.after.call(this,a)};o.fill=function(){var a=this.opts.fill;return k.isFunction(a)?a:function(b,e){return a[e%a.length]}};o.prepare=function(a,b){this.$svg||this.$el.hide().after(this.$svg=q("svg",{"class":"peity"}));return this.$svg.empty().data("peity",this).attr({height:b,width:a})};o.values=function(){return k.map(this.$el.text().split(this.opts.delimiter),function(a){return parseFloat(a)})};d.defaults={};d.graphers={};d.register=
+function(a,b,e){this.defaults[a]=b;this.graphers[a]=e};d.register("pie",{fill:["#ff9900","#fff4dd","#ffc66e"],radius:8},function(a){if(!a.delimiter){var b=this.$el.text().match(/[^0-9\.]/);a.delimiter=b?b[0]:","}b=k.map(this.values(),function(a){return 0i?n=s(h.min(e,0)):p=s(h.max(c,0)):o=1;o=p-n;0==o&&(o=1,0
';
+ }
+ }
+ },{
+ targets: 2,
+ title: 'name',
+ width: 200,
+ data: 'character',
+ render: {
+ _: 'name',
+ sort: 'name'
+ }
+ },{
+ targets: 3,
+ title: 'last login',
+ searchable: false,
+ width: 70,
+ className: ['text-right', 'separator-right'].join(' '),
+ data: 'character',
+ render: {
+ _: 'lastLogin',
+ sort: 'lastLogin'
+ },
+ createdCell: function(cell, cellData, rowData, rowIndex, colIndex){
+ $(cell).initTimestampCounter();
+ }
+ },{
+ targets: 4,
+ title: 'C ',
+ orderable: false,
+ searchable: false,
+ width: columnNumberWidth,
+ className: ['text-right', 'hidden-xs', 'hidden-sm'].join(' '),
+ data: 'systemCreate',
+ render: {
+ _: renderInlineChartColumn
+ }
+ },{
+ targets: 5,
+ title: 'U ',
+ orderable: false,
+ searchable: false,
+ width: columnNumberWidth,
+ className: ['text-right', 'hidden-xs', 'hidden-sm'].join(' '),
+ data: 'systemUpdate',
+ render: {
+ _: renderInlineChartColumn
+ }
+ },{
+ targets: 6,
+ title: 'D ',
+ orderable: false,
+ searchable: false,
+ width: columnNumberWidth,
+ className: ['text-right', 'hidden-xs', 'hidden-sm'].join(' '),
+ data: 'systemDelete',
+ render: {
+ _: renderInlineChartColumn
+ }
+ },{
+ targets: 7,
+ title: 'Σ ',
+ searchable: false,
+ width: 20,
+ className: ['text-right', 'hidden-xs', 'hidden-sm', 'separator-right'].join(' '),
+ data: 'systemSum',
+ render: {
+ _: renderNumericColumn
+ }
+ },{
+ targets: 8,
+ title: 'C ',
+ orderable: false,
+ searchable: false,
+ width: columnNumberWidth,
+ className: ['text-right', 'hidden-xs', 'hidden-sm'].join(' '),
+ data: 'connectionCreate',
+ render: {
+ _: renderInlineChartColumn
+ }
+ },{
+ targets: 9,
+ title: 'U ',
+ orderable: false,
+ searchable: false,
+ width: columnNumberWidth,
+ className: ['text-right', 'hidden-xs', 'hidden-sm'].join(' '),
+ data: 'connectionUpdate',
+ render: {
+ _: renderInlineChartColumn
+ }
+ },{
+ targets: 10,
+ title: 'D ',
+ orderable: false,
+ searchable: false,
+ width: columnNumberWidth,
+ className: ['text-right', 'hidden-xs', 'hidden-sm'].join(' '),
+ data: 'connectionDelete',
+ render: {
+ _: renderInlineChartColumn
+ }
+ },{
+ targets: 11,
+ title: 'Σ ',
+ searchable: false,
+ width: 20,
+ className: ['text-right', 'hidden-xs', 'hidden-sm', 'separator-right'].join(' '),
+ data: 'connectionSum',
+ render: {
+ _: renderNumericColumn
+ }
+ },{
+ targets: 12,
+ title: 'C ',
+ orderable: false,
+ searchable: false,
+ width: columnNumberWidth,
+ className: ['text-right', 'hidden-xs', 'hidden-sm'].join(' '),
+ data: 'signatureCreate',
+ render: {
+ _: renderInlineChartColumn
+ }
+ },{
+ targets: 13,
+ title: 'U ',
+ orderable: false,
+ searchable: false,
+ width: columnNumberWidth,
+ className: ['text-right', 'hidden-xs', 'hidden-sm'].join(' '),
+ data: 'signatureUpdate',
+ render: {
+ _: renderInlineChartColumn
+ }
+ },{
+ targets: 14,
+ title: 'D ',
+ orderable: false,
+ searchable: false,
+ width: columnNumberWidth,
+ className: ['text-right', 'hidden-xs', 'hidden-sm'].join(' '),
+ data: 'signatureDelete',
+ render: {
+ _: renderInlineChartColumn
+ }
+ },{
+ targets: 15,
+ title: 'Σ ',
+ searchable: false,
+ width: 20,
+ className: ['text-right', 'hidden-xs', 'hidden-sm', 'separator-right'].join(' '),
+ data: 'signatureSum',
+ render: {
+ _: renderNumericColumn
+ }
+ },{
+ targets: 16,
+ title: 'Σ ',
+ searchable: false,
+ width: 20,
+ className: 'text-right',
+ data: 'totalSum',
+ render: {
+ _: renderNumericColumn
+ }
+ }
+ ],
+ initComplete: function(settings){
+ var tableApi = this.api();
+
+ // initial statistics data request
+ var requestData = getRequestDataFromTabPanels(dialogElement);
+ getStatsData(requestData, {tableApi: tableApi, callback: drawStatsTable});
+ },
+ drawCallback: function(settings){
+ this.api().rows().nodes().to$().each(function(i, row){
+ $(row).find('.' + config.statsLineChartClass).peity('line', {
+ fill: 'transparent',
+ height: 18,
+ min: 0,
+ width: 50
+ });
+ });
+ },
+ footerCallback: function ( row, data, start, end, display ) {
+ var api = this.api();
+ var sumColumnIndexes = [7, 11, 15, 16];
+
+ // column data for "sum" columns over this page
+ var pageTotalColumns = api
+ .columns( sumColumnIndexes, { page: 'current'} )
+ .data();
+
+ // sum columns for "total" sum
+ pageTotalColumns.each(function(colData, index){
+ pageTotalColumns[index] = colData.reduce(function(a, b){
+ return a + b;
+ }, 0);
+ });
+
+ $(sumColumnIndexes).each(function(index, value){
+ $( api.column( value ).footer() ).text( renderNumericColumn(pageTotalColumns[index]) );
+ });
+ },
+ data: [] // will be added dynamic
+ });
+
+ statsTable.on('order.dt search.dt', function(){
+ statsTable.column(0, {search:'applied', order:'applied'}).nodes().each(function(cell, i){
+ $(cell).html( (i + 1) + '. ');
+ });
+ }).draw();
+
+ var tooltipElements = dialogElement.find('[data-toggle="tooltip"]');
+ tooltipElements.tooltip();
+ };
+
+ /**
+ * request raw statistics data and execute callback
+ * @param requestData
+ * @param context
+ */
+ var getStatsData = function(requestData, context){
+
+ context.dynamicArea = $('#' + config.statsContainerId + ' .pf-dynamic-area');
+ context.dynamicArea.showLoadingAnimation();
+
+ $.ajax({
+ type: 'POST',
+ url: Init.path.getStatisticsData,
+ data: requestData,
+ dataType: 'json',
+ context: context
+ }).done(function(data){
+ this.dynamicArea.hideLoadingAnimation();
+
+ this.callback(data);
+ }).fail(function( jqXHR, status, error) {
+ var reason = status + ' ' + error;
+ Util.showNotify({title: jqXHR.status + ': loadStatistics', text: reason, type: 'warning'});
+ });
+ };
+
+ /**
+ * update dataTable with response data
+ * update "header"/"filter" elements in dialog
+ * @param responseData
+ */
+ var drawStatsTable = function(responseData){
+ var dialogElement = $('#' + config.statsDialogId);
+
+ // update filter/header -----------------------------------------------------------------------------
+ var navigationListElements = $('.' + config.dialogNavigationClass);
+ navigationListElements.find('a[data-type="typeId"][data-value="' + responseData.typeId + '"]').tab('show');
+ navigationListElements.find('a[data-type="period"][data-value="' + responseData.period + '"]').tab('show');
+
+ // update period pagination -------------------------------------------------------------------------
+ var prevButton = dialogElement.find('.' + config.dialogNavigationPrevClass);
+ prevButton.data('newOffset', responseData.prev);
+ prevButton.find('span').text('Week ' + responseData.prev.week + ', ' + responseData.prev.year);
+ prevButton.css('visibility', 'visible');
+
+ var nextButton = dialogElement.find('.' + config.dialogNavigationNextClass);
+ if(responseData.next){
+ nextButton.data('newOffset', responseData.next);
+ nextButton.find('span').text('Week ' + responseData.next.week + ', ' + responseData.next.year);
+ nextButton.css('visibility', 'visible');
+ }else{
+ nextButton.css('visibility', 'hidden');
+ }
+
+ // update current period information label ----------------------------------------------------------
+ // if period == "weekly" there is no "offset" -> just a single week
+ var offsetText = 'Week ' + responseData.start.week + ', ' + responseData.start.year;
+ if(responseData.period !== 'weekly'){
+ offsetText += ' ' +
+ 'Week ' + responseData.offset.week + ', ' + responseData.offset.year;
+ }
+ dialogElement.find('.' + config.dialogNavigationOffsetClass)
+ .data('start', responseData.start)
+ .data('period', responseData.period)
+ .html(offsetText);
+
+ // clear and (re)-fill table ------------------------------------------------------------------------
+ var formattedData = formatStatisticsData(responseData);
+ this.tableApi.clear();
+ this.tableApi.rows.add(formattedData).draw();
+ };
+
+ /**
+ * format statistics data for dataTable
+ * -> e.g. format inline-chart data
+ * @param statsData
+ * @returns {Array}
+ */
+ var formatStatisticsData = function(statsData){
+ var formattedData = [];
+ var yearStart = statsData.start.year;
+ var weekStart = statsData.start.week;
+ var weekCount = statsData.weekCount;
+ var yearWeeks = statsData.yearWeeks;
+
+ var tempRand = function(min, max){
+ return Math.random() * (max - min) + min;
+ };
+
+ // format/sum week statistics data for inline charts
+ var formatWeekData = function(weeksData){
+ var currentYear = yearStart;
+ var currentWeek = weekStart;
+
+ var formattedWeeksData = {
+ systemCreate: [],
+ systemUpdate: [],
+ systemDelete: [],
+ connectionCreate: [],
+ connectionUpdate: [],
+ connectionDelete: [],
+ signatureCreate: [],
+ signatureUpdate: [],
+ signatureDelete: [],
+ systemSum: 0,
+ connectionSum: 0,
+ signatureSum: 0
+ };
+
+ for(let i = 0; i < weekCount; i++){
+ let yearWeekProp = currentYear + '' + currentWeek;
+
+ if(weeksData.hasOwnProperty( yearWeekProp )){
+ let weekData = weeksData[ yearWeekProp ];
+
+ // system -------------------------------------------------------------------------------
+ formattedWeeksData.systemCreate.push( weekData.systemCreate );
+ formattedWeeksData.systemSum += parseInt( weekData.systemCreate );
+
+ formattedWeeksData.systemUpdate.push( weekData.systemUpdate );
+ formattedWeeksData.systemSum += parseInt( weekData.systemUpdate );
+
+ formattedWeeksData.systemDelete.push( weekData.systemDelete );
+ formattedWeeksData.systemSum += parseInt( weekData.systemDelete );
+
+ // connection ---------------------------------------------------------------------------
+ formattedWeeksData.connectionCreate.push( weekData.connectionCreate );
+ formattedWeeksData.connectionSum += parseInt( weekData.connectionCreate );
+
+ formattedWeeksData.connectionUpdate.push( weekData.connectionUpdate );
+ formattedWeeksData.connectionSum += parseInt( weekData.connectionUpdate );
+
+ formattedWeeksData.connectionDelete.push( weekData.connectionDelete );
+ formattedWeeksData.connectionSum += parseInt( weekData.connectionDelete );
+
+ // signature ----------------------------------------------------------------------------
+ formattedWeeksData.signatureCreate.push( weekData.signatureCreate );
+ formattedWeeksData.signatureSum += parseInt( weekData.signatureCreate );
+
+ formattedWeeksData.signatureUpdate.push( weekData.signatureUpdate );
+ formattedWeeksData.signatureSum += parseInt( weekData.signatureUpdate );
+
+ formattedWeeksData.signatureDelete.push( weekData.signatureDelete );
+ formattedWeeksData.signatureSum += parseInt( weekData.signatureDelete );
+ }else{
+ // system -------------------------------------------------------------------------------
+ formattedWeeksData.systemCreate.push(0);
+ formattedWeeksData.systemUpdate.push(0);
+ formattedWeeksData.systemDelete.push(0);
+
+ // connection ---------------------------------------------------------------------------
+ formattedWeeksData.connectionCreate.push(0);
+ formattedWeeksData.connectionUpdate.push(0);
+ formattedWeeksData.connectionDelete.push(0);
+
+ // signature ----------------------------------------------------------------------------
+ formattedWeeksData.signatureCreate.push(0);
+ formattedWeeksData.signatureUpdate.push(0);
+ formattedWeeksData.signatureDelete.push(0);
+ }
+
+ currentWeek++;
+
+ if( currentWeek > yearWeeks[currentYear] ){
+ currentWeek = 1;
+ currentYear++;
+ }
+ }
+
+ // system ---------------------------------------------------------------------------------------
+ formattedWeeksData.systemCreate = formattedWeeksData.systemCreate.join(',');
+ formattedWeeksData.systemUpdate = formattedWeeksData.systemUpdate.join(',');
+ formattedWeeksData.systemDelete = formattedWeeksData.systemDelete.join(',');
+
+ // connection -----------------------------------------------------------------------------------
+ formattedWeeksData.connectionCreate = formattedWeeksData.connectionCreate.join(',');
+ formattedWeeksData.connectionUpdate = formattedWeeksData.connectionUpdate.join(',');
+ formattedWeeksData.connectionDelete = formattedWeeksData.connectionDelete.join(',');
+
+ // signature ------------------------------------------------------------------------------------
+ formattedWeeksData.signatureCreate = formattedWeeksData.signatureCreate.join(',');
+ formattedWeeksData.signatureUpdate = formattedWeeksData.signatureUpdate.join(',');
+ formattedWeeksData.signatureDelete = formattedWeeksData.signatureDelete.join(',');
+
+ return formattedWeeksData;
+ };
+
+ $.each(statsData.statistics, function(characterId, data){
+
+ var formattedWeeksData = formatWeekData(data.weeks);
+
+ var rowData = {
+ character: {
+ id: characterId,
+ name: data.name,
+ lastLogin: data.lastLogin
+ },
+ systemCreate: {
+ type: 'C',
+ data: formattedWeeksData.systemCreate
+ },
+ systemUpdate: {
+ type: 'U',
+ data: formattedWeeksData.systemUpdate
+ },
+ systemDelete: {
+ type: 'D',
+ data: formattedWeeksData.systemDelete
+ },
+ systemSum: formattedWeeksData.systemSum,
+ connectionCreate: {
+ type: 'C',
+ data: formattedWeeksData.connectionCreate
+ },
+ connectionUpdate: {
+ type: 'U',
+ data: formattedWeeksData.connectionUpdate
+ },
+ connectionDelete: {
+ type: 'D',
+ data: formattedWeeksData.connectionDelete
+ },
+ connectionSum: formattedWeeksData.connectionSum,
+ signatureCreate: {
+ type: 'C',
+ data: formattedWeeksData.signatureCreate
+ },
+ signatureUpdate: {
+ type: 'U',
+ data: formattedWeeksData.signatureUpdate
+ },
+ signatureDelete: {
+ type: 'D',
+ data: formattedWeeksData.signatureDelete
+ },
+ signatureSum: formattedWeeksData.signatureSum,
+ totalSum: formattedWeeksData.systemSum + formattedWeeksData.connectionSum + formattedWeeksData.signatureSum
+ };
+
+ formattedData.push(rowData);
+ });
+
+ return formattedData;
+ };
+
+ /**
+ *
+ * @param dialogElement
+ * @returns {{}}
+ */
+ var getRequestDataFromTabPanels = function(dialogElement){
+ var requestData = {};
+
+ // get data from "tab" panel links ------------------------------------------------------------------
+ var navigationListElements = dialogElement.find('.' + config.dialogNavigationClass);
+ navigationListElements.find('.' + config.dialogNavigationListItemClass + '.active a').each(function(){
+ var linkElement = $(this);
+ requestData[linkElement.data('type')]= linkElement.data('value');
+ });
+
+ // get current period (no offset) data (if available) -----------------------------------------------
+ var navigationOffsetElement = dialogElement.find('.' + config.dialogNavigationOffsetClass);
+ var startData = navigationOffsetElement.data('start');
+ var periodOld = navigationOffsetElement.data('period');
+
+ // if period switch was detected
+ // -> "year" and "week" should not be send
+ // -> start from "now"
+ if(
+ requestData.period === periodOld &&
+ startData
+ ){
+ requestData.year = startData.year;
+ requestData.week = startData.week;
+ }
+
+ return requestData;
+ };
+
+ /**
+ * check if "activity log" type is enabled for a group
+ * @param type
+ * @returns {boolean}
+ */
+ var isTabTypeEnabled = function(type){
+ var enabled = false;
+
+ switch(type){
+ case 'private':
+ if(Init.activityLogging.character){
+ enabled = true;
+ }
+ break;
+ case 'corporation':
+ if(
+ Init.activityLogging.corporation &&
+ Util.getCurrentUserInfo('corporationId')
+ ){
+ enabled = true;
+ }
+ break;
+ case 'alliance':
+ if(
+ Init.activityLogging.alliance &&
+ Util.getCurrentUserInfo('allianceId')
+ ){
+ enabled = true;
+ }
+ break;
+ }
+
+ return enabled;
+ };
+
+ /**
+ * show activity stats dialog
+ */
+ $.fn.showStatsDialog = function(){
+ requirejs(['text!templates/dialog/stats.html', 'mustache', 'peityInlineChart'], function(template, Mustache) {
+
+ var data = {
+ id: config.statsDialogId,
+ dialogNavigationClass: config.dialogNavigationClass,
+ dialogNavLiClass: config.dialogNavigationListItemClass,
+ enablePrivateTab: isTabTypeEnabled('private'),
+ enableCorporationTab: isTabTypeEnabled('corporation'),
+ enableAllianceTab: isTabTypeEnabled('alliance'),
+ statsContainerId: config.statsContainerId,
+ statsTableId: config.statsTableId,
+ dialogNavigationOffsetClass: config.dialogNavigationOffsetClass,
+ dialogNavigationPrevClass: config.dialogNavigationPrevClass,
+ dialogNavigationNextClass: config.dialogNavigationNextClass
+ };
+
+ var content = Mustache.render(template, data);
+
+ var statsDialog = bootbox.dialog({
+ title: 'Statistics',
+ message: content,
+ size: 'large',
+ show: false,
+ buttons: {
+ close: {
+ label: 'close',
+ className: 'btn-default'
+ }
+ }
+ });
+
+ // model events
+ statsDialog.on('show.bs.modal', function(e) {
+ var dialogElement = $(e.target);
+
+ initStatsTable(dialogElement);
+ });
+
+ // Tab module events
+ statsDialog.find('a[data-toggle="tab"]').on('show.bs.tab', function (e, b, c) {
+ if( $(e.target).parent().hasClass('disabled') ){
+ // no action on "disabled" tabs
+ return false;
+ }
+ });
+
+ statsDialog.find('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
+ var requestData = getRequestDataFromTabPanels(statsDialog);
+ var tableApi = statsDialog.find('#' + config.statsTableId).DataTable();
+
+ getStatsData(requestData, {tableApi: tableApi, callback: drawStatsTable});
+ });
+
+ // offset change links
+ statsDialog.find('.' + config.dialogNavigationPrevClass + ', .' + config.dialogNavigationNextClass).on('click', function(){
+ var offsetData = $(this).data('newOffset');
+ if(offsetData){
+ // this should NEVER fail!
+ // get "base" request data (e.g. typeId, period)
+ // --> overwrite period data with new period data
+ var tmpRequestData = getRequestDataFromTabPanels(statsDialog);
+ var requestData = $.extend({}, tmpRequestData, offsetData);
+ var tableApi = statsDialog.find('#' + config.statsTableId).DataTable();
+
+ getStatsData(requestData, {tableApi: tableApi, callback: drawStatsTable});
+ }
+ });
+
+ // show dialog
+ statsDialog.modal('show');
+ });
+ };
+});
\ No newline at end of file
diff --git a/public/js/v1.1.6/app/ui/system_route.js b/public/js/v1.1.6/app/ui/system_route.js
index 986ca8bd..08098284 100644
--- a/public/js/v1.1.6/app/ui/system_route.js
+++ b/public/js/v1.1.6/app/ui/system_route.js
@@ -316,7 +316,7 @@ define([
var settingsDialog = bootbox.dialog({
title: 'Route settings',
message: content,
- show: false,
+ show: false,
buttons: {
close: {
label: 'cancel',
@@ -444,7 +444,6 @@ define([
data: requestData,
context: context
}).done(function(routesData){
-
this.moduleElement.hideLoadingAnimation();
// execute callback
@@ -886,7 +885,14 @@ define([
};
-
+ /**
+ * draw route table
+ * @param mapId
+ * @param moduleElement
+ * @param systemFromData
+ * @param routesTable
+ * @param systemsTo
+ */
var drawRouteTable = function(mapId, moduleElement, systemFromData, routesTable, systemsTo){
var requestRouteData = [];
var currentTimestamp = Util.getServerTime().getTime();
diff --git a/public/js/v1.1.6/lib/jquery.peity.min.js b/public/js/v1.1.6/lib/jquery.peity.min.js
new file mode 100644
index 00000000..3d0166ee
--- /dev/null
+++ b/public/js/v1.1.6/lib/jquery.peity.min.js
@@ -0,0 +1,13 @@
+// Peity jQuery plugin version 3.2.0
+// (c) 2015 Ben Pickles
+//
+// http://benpickles.github.io/peity
+//
+// Released under MIT license.
+(function(k,w,h,v){var d=k.fn.peity=function(a,b){y&&this.each(function(){var e=k(this),c=e.data("_peity");c?(a&&(c.type=a),k.extend(c.opts,b)):(c=new x(e,a,k.extend({},d.defaults[a],e.data("peity"),b)),e.change(function(){c.draw()}).data("_peity",c));c.draw()});return this},x=function(a,b,e){this.$el=a;this.type=b;this.opts=e},o=x.prototype,q=o.svgElement=function(a,b){return k(w.createElementNS("http://www.w3.org/2000/svg",a)).attr(b)},y="createElementNS"in w&&q("svg",{})[0].createSVGRect;o.draw=
+function(){var a=this.opts;d.graphers[this.type].call(this,a);a.after&&a.after.call(this,a)};o.fill=function(){var a=this.opts.fill;return k.isFunction(a)?a:function(b,e){return a[e%a.length]}};o.prepare=function(a,b){this.$svg||this.$el.hide().after(this.$svg=q("svg",{"class":"peity"}));return this.$svg.empty().data("peity",this).attr({height:b,width:a})};o.values=function(){return k.map(this.$el.text().split(this.opts.delimiter),function(a){return parseFloat(a)})};d.defaults={};d.graphers={};d.register=
+function(a,b,e){this.defaults[a]=b;this.graphers[a]=e};d.register("pie",{fill:["#ff9900","#fff4dd","#ffc66e"],radius:8},function(a){if(!a.delimiter){var b=this.$el.text().match(/[^0-9\.]/);a.delimiter=b?b[0]:","}b=k.map(this.values(),function(a){return 0i?n=s(h.min(e,0)):p=s(h.max(c,0)):o=1;o=p-n;0==o&&(o=1,0Systems are represented by rectangle boxes on a map (more). - Pilots can interact with systems like "delete systems" (more) or + Pilots can interact with systems like "delete systems" (more) or "move systems" (more).
Locked systems can´t be selected (more), moved (more) - or deleted (more). + or deleted (more).
@@ -199,7 +199,7 @@
Any system that is not "Locked" (more) can be deleted from a map.
@@ -263,7 +263,7 @@@@ -276,7 +276,7 @@ Let your mates know about critical connections that should be mass-saved (e.g. H security exits) (more).
-Connections can be detached by several ways.
@@ -322,7 +322,7 @@ click the browser tab were pathfinder is open. Then press ctrl + v.Signatures can be detached by several ways.
diff --git a/public/templates/dialog/stats.html b/public/templates/dialog/stats.html new file mode 100644 index 00000000..de43e1e4 --- /dev/null +++ b/public/templates/dialog/stats.html @@ -0,0 +1,117 @@ +| character | + + + +sum | +|||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + |
| + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + |
I am playing EVE Online since almost 4 years. The majority of time (3+ years), my characters were a part of the "No Holes Barred" alliance. @@ -728,7 +756,7 @@
If you are planning to get deeper into the project or even think about hosting it on your own webserver, you should be aware of some important key points. Pathfinder is not comparable with any "out of the box" web applications or common CMS systems that come along with an auto-install feature. diff --git a/public/templates/view/setup.html b/public/templates/view/setup.html index d783bac1..243d1a3b 100644 --- a/public/templates/view/setup.html +++ b/public/templates/view/setup.html @@ -157,7 +157,7 @@
| {{@information.label}} | @@ -165,7 +165,7 @@
| {{@environmentData.label}} | @@ -251,7 +251,7 @@
| Status | +
+ |
+
| Max. shared users (private map) | +Max. shared users (private maps) | {{ @PATHFINDER.MAP.PRIVATE.MAX_SHARED }} |
| Max. shared users (corporation map) | +Max. shared users (corporation maps) | {{ @PATHFINDER.MAP.CORPORATION.MAX_SHARED }} |
| Max. shared users (alliance map) | +Max. shared users (alliance maps) | {{ @PATHFINDER.MAP.ALLIANCE.MAX_SHARED }} |
| Private map | +Private maps | {{ @PATHFINDER.MAP.PRIVATE.LIFETIME }} |
| Corporation map | +Corporation maps | {{ @PATHFINDER.MAP.CORPORATION.LIFETIME }} |
| Alliance map | +Alliance maps | {{ @PATHFINDER.MAP.ALLIANCE.LIFETIME }} |
| Status | +Private maps |
- |
+
| Corporation maps | +
+ |
+ |
| Alliance maps | +
+ |