Merge pull request #883 from exodus4d/develop

v1.5.5
This commit is contained in:
Mark Friedrich
2019-12-09 17:53:04 +01:00
committed by GitHub
181 changed files with 5028 additions and 2150 deletions

View File

@@ -13,8 +13,8 @@
// Define globals exposed by Node.js.
"node": true,
// Allow ES6.
"esversion": 7,
// Allow ES8.
"esversion": 9,
/*
* ENFORCING OPTIONS

View File

@@ -1,7 +1,8 @@
[CRON]
log = TRUE
cli = TRUE
web = FALSE
log = TRUE
cli = TRUE
web = FALSE
silent = TRUE
[CRON.presets]
; run every minute
@@ -51,13 +52,13 @@ deleteMapData = Cron\MapUpdate->deleteMapData, @downtime
deleteAuthenticationData = Cron\CharacterUpdate->deleteAuthenticationData, @downtime
; delete expired cache files
deleteExpiredCacheData = Cron\Cache->deleteExpiredData, @downtime
deleteExpiredCacheData = Cron\Cache->deleteExpiredCacheData, @downtime
; delete old statistics (activity log) data
deleteStatisticsData = Cron\StatisticsUpdate->deleteStatisticsData, @weekly
; truncate map history log files
truncateMapHistoryLogFiles = Cron\MapHistory->truncateFiles, @halfHour
truncateMapHistoryLogFiles = Cron\MapHistory->truncateMapHistoryLogFiles, @halfHour
; sync "sovereignty" and "faction warfare" data from CCP´s ESI API
updateSovereigntyData = Cron\Universe->updateSovereigntyData, @halfPastHour

View File

@@ -10,10 +10,93 @@ namespace Controller\Api;
use Controller;
use lib\Config;
use lib\Cron;
use Model;
class Setup extends Controller\Controller {
/**
* get HTML table <tr>´s for all cronjobs
* @param \Base $f3
*/
public function cronTable(\Base $f3){
$return = (object) [];
$return->error = [];
$return->jobsData = Cron::instance()->getJobsConfig();
$return->html = $this->getCronHtml($return->jobsData);
echo json_encode($return);
}
/**
* toggle "isPaused" for a cronjob by its name
* @param \Base $f3
*/
public function cronPause(\Base $f3){
$postData = (array)$f3->get('POST');
$return = (object) [];
$return->error = [];
if($jobName = (string)$postData['job']){
$cron = Cron::instance();
if($job = $cron->getJob($jobName)){
if($job->valid()){
$job->isPaused = !$job->isPaused;
$job->save();
$return->jobsData = $cron->getJobsConfig([$jobName]);
$return->html = $this->getCronHtml($return->jobsData);
}
}
}
echo json_encode($return);
}
/**
* execute a cronjob by its name
* -> runs sync
* -> max execution time might be lower than CLI calls!
* @param \Base $f3
*/
public function cronExecute(\Base $f3){
$postData = (array)$f3->get('POST');
$return = (object) [];
$return->error = [];
if($jobName = (string)$postData['job']){
$cron = Cron::instance();
if($job = $cron->getJob($jobName)){
if($job->valid()){
$cron->execute($jobName, false);
$return->jobsData = $cron->getJobsConfig([$jobName]);
$return->html = $this->getCronHtml($return->jobsData);
}
}
}
echo json_encode($return);
}
/**
* get HTML for cronJobs
* @param array $jobsData
* @return string
*/
protected function getCronHtml(array $jobsData) : string {
$tplData = [
'cronConfig' => [
'jobs' => $jobsData,
'settings' => $this->getF3()->constants(Cron::instance(), 'DEFAULT_')
],
'tplCounter' => $this->counter(),
'tplConvertBytes' => function(){
return call_user_func_array([\lib\format\Number::instance(), 'bytesToString'], func_get_args());
}
];
return \Template::instance()->render('templates/ui/cron_table_row.html', null, $tplData, 0);
}
/**
* build search index from existing data (e.g. Systems)
* OR import data from ESI (e.g. Structures)

View File

@@ -9,6 +9,7 @@
namespace Controller\Api;
use Controller;
use lib\Config;
use Model\Pathfinder;
class System extends Controller\AccessController {
@@ -39,7 +40,7 @@ class System extends Controller\AccessController {
$cacheResponse = false;
// number of log entries in each table per system (24 = 24h)
$logEntryCount = 24;
$logEntryCount = Pathfinder\AbstractSystemApiBasicModel::DATA_COLUMN_COUNT;
$ttl = 60 * 10;
@@ -51,47 +52,47 @@ class System extends Controller\AccessController {
'factionKills' => 'SystemFactionKillModel'
];
$exists = false;
foreach($systemIds as $systemId){
$cacheKey = $this->getSystemGraphCacheKey($systemId);
if( !$f3->exists($cacheKey, $graphData )){
if(!$exists = $f3->exists($cacheKey, $graphData)){
$graphData = [];
$cacheSystem = false;
foreach($logTables as $label => $ModelClass){
$systemLogModel = Pathfinder\AbstractPathfinderModel::getNew($ModelClass);
foreach($logTables as $label => $className){
$systemLogModel = Pathfinder\AbstractSystemApiBasicModel::getNew($className);
$systemLogExists = false;
// 10min cache (could be up to 1h cache time)
$systemLogModel->getByForeignKey('systemId', $systemId);
if( !$systemLogModel->dry() ){
if($systemLogModel->valid()){
$systemLogExists = true;
$cacheSystem = true;
$cacheResponse = true;
}
$systemLogData = $systemLogModel->getData();
// podKills share graph with shipKills -> skip
if($label != 'podKills'){
$graphData[$label]['logExists'] = $systemLogExists;
$graphData[$label]['updated'] = $systemLogData->updated;
}
$counter = 0;
for( $i = $logEntryCount; $i >= 1; $i--){
$column = 'value' . $i;
$value = $systemLogExists ? $systemLogModel->$column : 0;
// ship and pod kills should be merged into one table
$logValueCount = range(0, $logEntryCount - 1);
foreach($logValueCount as $i){
if($label == 'podKills'){
$graphData['shipKills']['data'][$counter]['z'] = $value;
$graphData['shipKills']['data'][$i]['z'] = $systemLogData->values[$i];
}else{
$dataSet = [
'x' => ($i - 1) . 'h',
'y' => $value
$graphData[$label]['data'][] = [
'x' => ($logEntryCount - $i - 1) . 'h',
'y' => $systemLogData->values[$i]
];
$graphData[$label]['data'][] = $dataSet;
}
$counter++;
}
}
if($cacheSystem){
$f3->set($cacheKey, $graphData, $ttl);
}
@@ -104,7 +105,7 @@ class System extends Controller\AccessController {
if($cacheResponse){
// send client cache header
$f3->expire($ttl);
$f3->expire(Config::ttlLeft($exists, $ttl));
}
echo json_encode($graphsData);

View File

@@ -10,6 +10,7 @@ namespace Controller\Api;
use Controller;
use lib\Config;
use Model\Pathfinder;
use Exception;
@@ -119,6 +120,7 @@ class User extends Controller\Controller{
$cookieName = (string)$data['cookie'];
$return = (object) [];
$return->ccpImageServer = Config::getPathfinderData('api.ccp_image_server');
$return->error = [];
if( !empty($cookieData = $this->getCookieByName($cookieName) )){

View File

@@ -54,7 +54,7 @@ class AppController extends Controller {
parent::afterroute($f3);
// clear all SSO related temp data
if( $f3->exists(Ccp\Sso::SESSION_KEY_SSO) ){
if($f3->exists(Ccp\Sso::SESSION_KEY_SSO)){
$f3->clear(Ccp\Sso::SESSION_KEY_SSO);
}
}

View File

@@ -468,7 +468,7 @@ class Sso extends Api\User{
*/
$corporation = Pathfinder\AbstractPathfinderModel::getNew('CorporationModel');
$corporation->getById($corporationId, 0);
if( !$corporation->dry() ){
if($corporation->valid()){
$characterData->corporation = $corporation;
}
}
@@ -479,7 +479,7 @@ class Sso extends Api\User{
*/
$alliance = Pathfinder\AbstractPathfinderModel::getNew('AllianceModel');
$alliance->getById($allianceId, 0);
if( !$alliance->dry() ){
if($alliance->valid()){
$characterData->alliance = $alliance;
}
}

View File

@@ -93,7 +93,10 @@ class Controller {
}else{
$this->initResource($f3);
$this->setTemplate( Config::getPathfinderData('view.index') );
$this->setTemplate(Config::getPathfinderData('view.index'));
$f3->set('tplImage', \lib\format\Image::instance());
}
return true;
@@ -165,7 +168,7 @@ class Controller {
'font' => $f3->get('BASE') . '/public/fonts',
'document' => $f3->get('BASE') . '/public/templates',
'image' => $f3->get('BASE') . '/public/img'
]);
], true);
$resource->register('style', 'pathfinder');
@@ -178,6 +181,10 @@ class Controller {
$resource->register('font', 'fa-solid-900');
$resource->register('font', 'fa-brands-400');
$resource->register('url', self::getEnvironmentData('CCP_SSO_URL'), 'prerender');
$resource->register('url', Config::getPathfinderData('api.ccp_image_server'), 'dns-prefetch');
$resource->register('url', '//i.ytimg.com', 'dns-prefetch'); // YouTube tiny embed domain
$f3->set('tplResource', $resource);
}
@@ -805,6 +812,26 @@ class Controller {
Monolog::instance()->log();
}
/**
* simple counter with "static" store
* -> called within tpl render
* @return \Closure
*/
protected function counter() : \Closure {
$store = [];
return function(string $action = 'increment', string $type = 'default', $val = 0) use (&$store){
$return = null;
switch($action){
case 'increment': $store[$type]++; break;
case 'add': $store[$type] += (int)$val; break;
case 'get': $return = $store[$type] ? : null; break;
case 'reset': unset($store[$type]); break;
}
return $return;
};
}
/**
* get controller by class name
* -> controller class is searched within all controller directories

View File

@@ -13,6 +13,7 @@ use data\filesystem\Search;
use DB\SQL\Schema;
use DB\SQL\MySQL as MySQL;
use lib\Config;
use lib\Cron;
use lib\Util;
use Model\Pathfinder;
use Model\Universe;
@@ -59,6 +60,7 @@ class Setup extends Controller {
'PF' => [
'info' => [],
'models' => [
'Model\Pathfinder\CronModel',
'Model\Pathfinder\UserModel',
'Model\Pathfinder\AllianceModel',
'Model\Pathfinder\CorporationModel',
@@ -167,17 +169,10 @@ class Setup extends Controller {
// js view (file)
$f3->set('tplJsView', 'setup');
// simple counter (called within template)
$counter = [];
$f3->set('tplCounter', function(string $action = 'increment', string $type = 'default', $val = 0) use (&$counter){
$return = null;
switch($action){
case 'increment': $counter[$type]++; break;
case 'add': $counter[$type] += (int)$val; break;
case 'get': $return = $counter[$type]? : null; break;
case 'reset': unset($counter[$type]); break;
}
return $return;
$f3->set('tplCounter', $this->counter());
$f3->set('tplConvertBytes', function(){
return call_user_func_array([\lib\format\Number::instance(), 'bytesToString'], func_get_args());
});
// render view
@@ -264,6 +259,9 @@ class Setup extends Controller {
// WebSocket information
$f3->set('socketInformation', $this->getSocketInformation($f3));
// Cronjob ----------------------------------------------------------------------------------------------------
$f3->set('cronConfig', $this->getCronConfig($f3));
// Administration ---------------------------------------------------------------------------------------------
// Index information
$f3->set('indexInformation', $this->getIndexData($f3));
@@ -296,9 +294,12 @@ class Setup extends Controller {
'socket' => [
'icon' => 'fa-exchange-alt'
],
'cronjob' => [
'icon' => 'fa-user-clock'
],
'administration' => [
'icon' => 'fa-wrench'
],
]
];
return $config;
@@ -761,20 +762,20 @@ class Setup extends Controller {
],
'maxMemory' => [
'label' => 'maxmemory',
'required' => $this->convertBytes($f3->get('REQUIREMENTS.REDIS.MAX_MEMORY')),
'version' => $this->convertBytes($redisMemoryInfo['maxmemory']),
'required' => \lib\format\Number::instance()->bytesToString($f3->get('REQUIREMENTS.REDIS.MAX_MEMORY')),
'version' => \lib\format\Number::instance()->bytesToString($redisMemoryInfo['maxmemory']),
'check' => $redisMemoryInfo['maxmemory'] >= $f3->get('REQUIREMENTS.REDIS.MAX_MEMORY'),
'tooltip' => 'Max memory limit for Redis'
],
'usedMemory' => [
'label' => 'used_memory',
'version' => $this->convertBytes($redisMemoryInfo['used_memory']),
'version' => \lib\format\Number::instance()->bytesToString($redisMemoryInfo['used_memory']),
'check' => $redisMemoryInfo['used_memory'] < $redisMemoryInfo['maxmemory'],
'tooltip' => 'Current memory used by Redis'
],
'usedMemoryPeak' => [
'label' => 'used_memory_peak',
'version' => $this->convertBytes($redisMemoryInfo['used_memory_peak']),
'version' => \lib\format\Number::instance()->bytesToString($redisMemoryInfo['used_memory_peak']),
'check' => $redisMemoryInfo['used_memory_peak'] <= $redisMemoryInfo['maxmemory'],
'tooltip' => 'Peak memory used by Redis'
],
@@ -1139,6 +1140,8 @@ class Setup extends Controller {
'fieldConf' => $tableConfig['fieldConf'],
'exists' => false,
'empty' => true,
'requiredCharset' => $tableConfig['charset'],
'requiredCollation' => $tableConfig['charset'] . '_unicode_ci',
'foreignKeys' => []
];
}
@@ -1157,7 +1160,8 @@ class Setup extends Controller {
// check each table for changes
foreach($requiredTables as $requiredTableName => $data){
$tableCharset = null;
$tableCollation = null;
$tableExists = false;
$tableRows = 0;
// Check if table status is OK (no errors/warnings,..)
@@ -1173,6 +1177,14 @@ class Setup extends Controller {
// get row count
$tableRows = $db->getRowCount($requiredTableName);
$tableStatus = $db->getTableStatus($requiredTableName);
if(
!empty($tableStatus['Collation']) &&
($statusVal = strstr($tableStatus['Collation'], '_', true)) !== false
){
$tableCharset = $statusVal;
$tableCollation = $tableStatus['Collation'];
}
// find deprecated columns that are no longer needed ------------------------------------------
$deprecatedColumnNames = array_diff(array_keys($currentColumns), array_keys($data['fieldConf']), ['id']);
@@ -1365,6 +1377,8 @@ class Setup extends Controller {
}
$dbStatusCheckCount += $tableStatusCheckCount;
$requiredTables[$requiredTableName]['currentCharset'] = $tableCharset;
$requiredTables[$requiredTableName]['currentCollation'] = $tableCollation;
$requiredTables[$requiredTableName]['rows'] = $tableRows;
$requiredTables[$requiredTableName]['exists'] = $tableExists;
$requiredTables[$requiredTableName]['statusCheckCount'] = $tableStatusCheckCount;
@@ -1659,13 +1673,59 @@ class Setup extends Controller {
return $socketInformation;
}
/**
* get cronjob config
* @param \Base $f3
* @return array
*/
protected function getCronConfig(\Base $f3) : array {
$cron = Cron::instance();
$cronConf = [
'log' => [
'label' => 'LOG',
'required' => $f3->get('REQUIREMENTS.CRON.LOG'),
'version' => $f3->get('CRON.log'),
'check' => $f3->get('CRON.log') == $f3->get('REQUIREMENTS.CRON.LOG'),
'tooltip' => 'Write default cron.log'
],
'cli' => [
'label' => 'CLI',
'required' => $f3->get('REQUIREMENTS.CRON.CLI'),
'version' => $f3->get('CRON.cli'),
'check' => $f3->get('CRON.cli') == $f3->get('REQUIREMENTS.CRON.CLI'),
'tooltip' => 'Jobs can be triggered by CLI. Must be set on Unix where "crontab -e" config is used'
],
'web' => [
'label' => 'WEB',
'version' => (int)$f3->get('CRON.web'),
'check' => true,
'tooltip' => 'Jobs can be triggered by URL. Could be useful if jobs should be triggered by e.g. 3rd party app. Secure "/cron" url if active!'
],
'silent' => [
'label' => 'SILENT',
'version' => (int)$f3->get('CRON.silent'),
'check' => true,
'tooltip' => 'Write job execution status to STDOUT if job completes'
]
];
$config = [
'checkCronConfig' => $cronConf,
'settings' => $f3->constants($cron, 'DEFAULT_'),
'jobs' => $cron->getJobsConfig()
];
return $config;
}
/**
* get indexed (cache) data information
* @param \Base $f3
* @return array
* @throws \Exception
*/
protected function getIndexData(\Base $f3){
protected function getIndexData(\Base $f3) : array {
// active DB and tables are required for obtain index data
if(!$this->databaseHasError){
/**
@@ -1890,7 +1950,7 @@ class Setup extends Controller {
}
$bytesAll += $bytes;
$dirAll[$key]['size'] = ($maxHit ? '>' : '') . $this->convertBytes($bytes);
$dirAll[$key]['size'] = ($maxHit ? '>' : '') . \lib\format\Number::instance()->bytesToString($bytes);
$dirAll[$key]['task'] = [
[
'action' => http_build_query([
@@ -1905,7 +1965,7 @@ class Setup extends Controller {
}
return [
'sizeAll' => ($maxHitAll ? '>' : '') . $this->convertBytes($bytesAll),
'sizeAll' => ($maxHitAll ? '>' : '') . \lib\format\Number::instance()->bytesToString($bytesAll),
'dirAll' => $dirAll
];
}
@@ -1957,20 +2017,4 @@ class Setup extends Controller {
}
}
}
/**
* convert Bytes to string + suffix
* @param int $bytes
* @param int $precision
* @return string
*/
protected function convertBytes($bytes, $precision = 2){
$result = '0';
if($bytes){
$base = log($bytes, 1024);
$suffixes = array('', 'KB', 'M', 'GB', 'TB');
$result = round(pow(1024, $base - floor($base)), $precision) .''. $suffixes[(int)floor($base)];
}
return $result;
}
}

View File

@@ -8,11 +8,19 @@
namespace cron;
use Model\Pathfinder;
abstract class AbstractCron {
/**
* default "base" log message text
* -> generic information data
*/
const LOG_TEXT_BASE = '%4s/%-4s %6s done, %5s total, %8s peak, %9s exec';
/**
* default max_execution_time for cronJobs
// -> should be less then execution period
* -> should be less then execution period
*/
const DEFAULT_MAX_EXECUTION_TIME = 50;
@@ -22,6 +30,19 @@ abstract class AbstractCron {
*/
const DEFAULT_EXECUTION_TIME_THRESHOLD = 3;
/**
* started jobs
* @var Pathfinder\CronModel[]
*/
protected $activeCron = [];
/**
* disables log file write entry for some cronJobs
* -> either job runs too frequently, or no relevant data available for logging
* @var array
*/
protected $logDisabled = [];
/**
* set max execution time for cronJbs
* -> Default CLI execution time is 0 == infinite!
@@ -61,4 +82,129 @@ abstract class AbstractCron {
return $timeLeft;
}
/**
* log cronjob exec state on start
* @param string $job
* @param bool $logging
*/
protected function logStart(string $job, bool $logging = true){
$this->setMaxExecutionTime();
$cron = \lib\Cron::instance();
if(isset($cron->jobs[$job])){
// set "start" date for current cronjob
$jobConf = $cron->getJobDataFromConf($cron->jobs[$job]);
$jobConf['lastExecStart'] = $_SERVER['REQUEST_TIME_FLOAT'];
if(($cronModel = $cron->registerJob($job, $jobConf)) instanceof Pathfinder\CronModel){
$this->activeCron[$job] = $cronModel;
}
}
if(!$logging){
$this->logDisabled[] = $job;
}
}
/**
* log cronjob exec state on finish
* @param string $job
* @param int $total
* @param int $count
* @param int $importCount
* @param int $offset
* @param string $logText
*/
protected function logEnd(string $job, int $total = 0, int $count = 0, int $importCount = 0, int $offset = 0, string $logText = ''){
$execEnd = microtime(true);
$memPeak = memory_get_peak_usage();
$state = [
'total' => $total,
'count' => $count,
'importCount' => $importCount,
'offset' => $offset,
'loop' => 1,
'percent' => $total ? round(100 / $total * ($count + $offset), 1) : 100
];
if(isset($this->activeCron[$job])){
if($lastState = $this->activeCron[$job]->lastExecState){
if(isset($lastState['loop']) && $offset){
$state['loop'] = (int)$lastState['loop'] + 1;
}
}
$jobConf = [
'lastExecEnd' => $execEnd,
'lastExecMemPeak' => $memPeak,
'lastExecState' => $state
];
$this->activeCron[$job]->setData($jobConf);
$this->activeCron[$job]->save();
unset($this->activeCron[$job]);
}
if(!in_array($job, $this->logDisabled)){
$this->writeLog($job, $memPeak, $execEnd, $state, $logText);
}
}
/**
* get either CLI GET params OR
* check for params from last run -> incremental import
* @param string $job
* @return array
*/
protected function getParams(string $job) : array {
$params = [];
// check for CLI GET params
$f3 = \Base::instance();
if($getParams = (array)$f3->get('GET')){
if(isset($getParams['offset'])){
$params['offset'] = (int)$getParams['offset'];
}
if(isset($getParams['length']) && (int)$getParams['length'] > 0){
$params['length'] = (int)$getParams['length'];
}
}
// .. or check for logged params from last exec state (DB entry)
if(empty($params) && isset($this->activeCron[$job])){
if($lastState = $this->activeCron[$job]->lastExecState){
if(isset($lastState['offset'])){
$params['offset'] = (int)$lastState['offset'];
}
if(isset($lastState['count'])){
$params['offset'] = (int)$params['offset'] + (int)$lastState['count'];
}
if(isset($lastState['loop'])){
$params['loop'] = (int)$lastState['loop'];
}
}
}
return $params;
}
/**
* write log file for $job
* @param string $job
* @param int $memPeak
* @param float $execEnd
* @param array $state
* @param string $logText for custom text
*/
private function writeLog(string $job, int $memPeak = 0, float $execEnd = 0, array $state = [], string $logText = ''){
$percent = number_format($state['percent'], 1) . '%';
$duration = number_format(round($execEnd - $_SERVER['REQUEST_TIME_FLOAT'], 3), 3) . 's';
$log = new \Log('cron_' . $job . '.log');
$text = sprintf(self::LOG_TEXT_BASE,
$state['count'], $state['importCount'], $percent, $state['total'],
\lib\format\Number::instance()->bytesToString($memPeak), $duration
);
$text .= $logText ? $logText: '';
$log->write($text);
}
}

View File

@@ -12,7 +12,10 @@ use data\filesystem\Search;
class Cache extends AbstractCron {
const LOG_TEXT = '%s [%\'_10s] files, size [%\'_10s] byte, not writable [%\'_10s] files, errors [%\'_10s], exec (%.3Fs)';
/**
* log text
*/
const LOG_TEXT = ', size [%\'_10s] byte, not writable [%\'_10s] files, errors [%\'_10s]';
/**
* default max expire for files (seconds)
@@ -33,9 +36,8 @@ class Cache extends AbstractCron {
* >> php index.php "/cron/deleteExpiredCacheData"
* @param \Base $f3
*/
function deleteExpiredData(\Base $f3){
$this->setMaxExecutionTime();
$time_start = microtime(true);
function deleteExpiredCacheData(\Base $f3){
$this->logStart(__FUNCTION__);
// cache dir (dir is recursively searched...)
$cacheDir = $f3->get('TEMP');
@@ -43,6 +45,7 @@ class Cache extends AbstractCron {
$filterTime = (int)strtotime('-' . $this->getExpireMaxTime($f3) . ' seconds');
$expiredFiles = Search::getFilesByMTime($cacheDir, $filterTime, Search::DEFAULT_FILE_LIMIT);
$totalFiles = 0;
$deletedFiles = 0;
$deletedSize = 0;
$notWritableFiles = 0;
@@ -52,7 +55,8 @@ class Cache extends AbstractCron {
* @var $file \SplFileInfo
*/
if($file->isFile()){
if( $file->isWritable() ){
$totalFiles++;
if($file->isWritable()){
$tmpSize = $file->getSize();
if( unlink($file->getRealPath()) ){
$deletedSize += $tmpSize;
@@ -66,11 +70,13 @@ class Cache extends AbstractCron {
}
}
$execTime = microtime(true) - $time_start;
// Log --------------------------------------------------------------------------------------------------------
$total = $totalFiles;
$importCount = $total;
$count = $deletedFiles;
// Log ------------------------
$log = new \Log('cron_' . __FUNCTION__ . '.log');
$log->write( sprintf(self::LOG_TEXT, __FUNCTION__, $deletedFiles, $deletedSize, $notWritableFiles, $deleteErrors, $execTime) );
$text = sprintf(self::LOG_TEXT, $deletedSize, $notWritableFiles, $deleteErrors);
$this->logEnd(__FUNCTION__, $total, $count, $importCount, 0, $text);
}
}

View File

@@ -11,7 +11,10 @@ use lib\db\SQL;
class CcpSystemsUpdate extends AbstractCron {
const LOG_TEXT = '%s prepare table (%.3F s), jump (%.3F s), kill (%.3F s), update all (%.3F s)';
/**
* log text
*/
const LOG_TEXT = ' → [%.3Fs prepare table, %.3Fs jump, %.3Fs kill, %.3Fs update all]';
/**
* table names for all system log tables
@@ -37,47 +40,50 @@ class CcpSystemsUpdate extends AbstractCron {
/**
* check all system log tables for the correct number of system entries that will be locked
* @param \Base $f3
* @return array
* @return int[]
*/
private function prepareSystemLogTables(\Base $f3) : array {
$systemsData = [];
$systemIds = [];
// get all available systems from "universe" DB
$universeDB = $f3->DB->getDB('UNIVERSE');
if($this->tableExists($universeDB, 'system')){
$systemsData = $universeDB->exec('SELECT
`id`
FROM
`system`
WHERE
`security` = :ns OR
`security` = :ls OR
`security` = :hs
',
$systemsData = $universeDB->exec('
SELECT
`id`
FROM
`system`
WHERE
`security` = :ns OR
`security` = :ls OR
`security` = :hs
',
[':ns' => '0.0', ':ls' => 'L', ':hs' => 'H']
);
$systemIds = array_map('intval', array_column($systemsData, 'id'));
sort($systemIds, SORT_NUMERIC);
$pfDB = $f3->DB->getDB('PF');
// insert systems into each log table if not exist
$pfDB->begin();
foreach($this->logTables as $tableName){
$pfDB->begin();
// insert systems into jump log table
$sqlInsertSystem = "INSERT IGNORE INTO " . $tableName . " (systemId)
VALUES(:systemId)";
$sqlInsertSystem = "INSERT IGNORE INTO " . $tableName . " (`systemId`) VALUES (:systemId)";
foreach($systemsData as $systemData){
$pfDB->exec($sqlInsertSystem, array(
':systemId' => $systemData['id']
), 0, false);
foreach($systemIds as $systemId){
$pfDB->exec($sqlInsertSystem, [
':systemId' => $systemId
], 0, false);
}
$pfDB->commit();
}
$pfDB->commit();
}
return $systemsData;
return $systemIds;
}
@@ -87,15 +93,26 @@ class CcpSystemsUpdate extends AbstractCron {
* @param \Base $f3
*/
function importSystemData(\Base $f3){
$this->setMaxExecutionTime();
$this->logStart(__FUNCTION__);
$params = $this->getParams(__FUNCTION__);
// prepare system jump log table ------------------------------------------------------------------------------
$time_start = microtime(true);
$systemsData = $this->prepareSystemLogTables($f3);
$systemIds = $this->prepareSystemLogTables($f3);
$time_end = microtime(true);
$execTimePrepareSystemLogTables = $time_end - $time_start;
$total = count($systemIds);
$offset = ($params['offset'] > 0 && $params['offset'] < $total) ? $params['offset'] : 0;
$systemIds = array_slice($systemIds, $offset, $params['length']);
$importCount = count($systemIds);
$count = 0;
// switch DB for data import..
/**
* @var $pfDB SQL
*/
$pfDB = $f3->DB->getDB('PF');
// get current jump data --------------------------------------------------------------------------------------
@@ -115,64 +132,57 @@ class CcpSystemsUpdate extends AbstractCron {
// update system log tables -----------------------------------------------------------------------------------
$time_start = microtime(true);
$pfDB->begin();
$logTableCount = count($this->logTables);
$logTableCounter = 0;
foreach($this->logTables as $key => $tableName){
$sql = "UPDATE
$tableName
SET
updated = now(),
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
";
$logTableCounter++;
$pfDB->begin();
foreach($systemsData as $systemData){
$systemId = $systemData['id'];
$sqlUpdateColumn = vsprintf("SELECT `systemId`, IF (lastUpdatedValue, lastUpdatedValue, DEFAULT (lastUpdatedValue)) AS `updateColumn` FROM %s", [
$pfDB->quotekey($tableName)
]);
$resUpdateColumns = $pfDB->exec($sqlUpdateColumn, null, 0);
$resUpdateColumns = array_column($resUpdateColumns, 'updateColumn', 'systemId');
foreach($systemIds as $systemId){
$column = 1;
if(isset($resUpdateColumns[$systemId])){
$column = (int)$resUpdateColumns[$systemId];
$column = (++$column > 24) ? 1 : $column;
}
// update data (if available)
$currentData = 0;
if( isset($systemValues[$systemId][$key]) ){
if(isset($systemValues[$systemId][$key])){
$currentData = (int)$systemValues[$systemId][$key];
}
$sql = vsprintf("UPDATE %s SET `updated` = NOW(), %s = :value, `lastUpdatedValue` = :updateColumn WHERE systemId = :systemId", [
$pfDB->quotekey($tableName),
$pfDB->quotekey('value' . $column)
]);
$pfDB->exec($sql, [
':systemId' => $systemId,
':updateColumn' => $column,
':value' => $currentData
], 0, false);
}
}
] , 0);
$pfDB->commit();
// system import is done if ALL related tables (4) were updated
if($logTableCounter === $logTableCount){
$count++;
}
}
$pfDB->commit();
}
$time_end = microtime(true);
$execTimeUpdateTables = $time_end - $time_start;
// Log --------------------------------------------------------------------------------------------------------
$log = new \Log('cron_' . __FUNCTION__ . '.log');
$log->write( sprintf(self::LOG_TEXT, __FUNCTION__, $execTimePrepareSystemLogTables, $execTimeGetJumpData, $execTimeGetKillData, $execTimeUpdateTables) );
$text = sprintf(self::LOG_TEXT, $execTimePrepareSystemLogTables, $execTimeGetJumpData, $execTimeGetKillData, $execTimeUpdateTables);
$this->logEnd(__FUNCTION__, $total, $count, $importCount, $offset, $text);
}
}

View File

@@ -41,7 +41,7 @@ class CharacterUpdate extends AbstractCron {
* @throws \Exception
*/
function deleteLogData(\Base $f3){
$this->setMaxExecutionTime();
$this->logStart(__FUNCTION__, false);
$logInactiveTime = $this->getCharacterLogInactiveTime($f3);
/**
@@ -58,7 +58,11 @@ class CharacterUpdate extends AbstractCron {
'limit' => self::CHARACTERS_UPDATE_LOGS_MAX
]);
$total = 0;
$count = 0;
if(is_object($characterLogs)){
$total = count($characterLogs);
foreach($characterLogs as $characterLog){
/**
* @var $characterLog Pathfinder\CharacterLogModel
@@ -72,13 +76,22 @@ class CharacterUpdate extends AbstractCron {
}else{
$characterLog->erase();
}
}else{
// no valid $accessToken. (e.g. ESI is down; or invalid `refresh_token` found
$characterLog->erase();
}
}else{
// character_log does not have a character assigned -> delete
$characterLog->erase();
}
$count++;
}
}
$importCount = $total;
$this->logEnd(__FUNCTION__, $total, $count, $importCount);
}
/**
@@ -88,7 +101,7 @@ class CharacterUpdate extends AbstractCron {
* @throws \Exception
*/
function cleanUpCharacterData(\Base $f3){
$this->setMaxExecutionTime();
$this->logStart(__FUNCTION__, false);
/**
* @var $characterModel Pathfinder\CharacterModel
@@ -109,6 +122,8 @@ class CharacterUpdate extends AbstractCron {
$character->save();
}
}
$this->logEnd(__FUNCTION__);
}
/**
@@ -119,7 +134,7 @@ class CharacterUpdate extends AbstractCron {
* @throws \Exception
*/
function deleteAuthenticationData(\Base $f3){
$this->setMaxExecutionTime();
$this->logStart(__FUNCTION__, false);
/**
* @var $authenticationModel Pathfinder\CharacterAuthenticationModel
@@ -136,6 +151,8 @@ class CharacterUpdate extends AbstractCron {
$authentication->erase();
}
}
$this->logEnd(__FUNCTION__);
}
}

View File

@@ -15,9 +15,9 @@ use data\filesystem\Search;
class MapHistory extends AbstractCron {
/**
* log msg for truncateFiles() cronjob
* log msg for truncateMapHistoryLogFiles() cronjob
*/
const LOG_TEXT = '%s [%4s] log files, [%s] truncated, [%4s] not writable, [%4s] read error, [%4s] write error, [%4s] rename error, [%4s] delete error, exec (%.3Fs)';
const LOG_TEXT = ', [%4s] log files, [%s] truncated, [%4s] not writable, [%4s] read error, [%4s] write error, [%4s] rename error, [%4s] delete error';
/**
* default log file size limit before truncate, bytes (1MB)
@@ -59,8 +59,8 @@ class MapHistory extends AbstractCron {
* >> php index.php "/cron/truncateMapHistoryLogFiles"
* @param \Base $f3
*/
function truncateFiles(\Base $f3){
$timeStart = microtime(true);
function truncateMapHistoryLogFiles(\Base $f3){
$this->logStart(__FUNCTION__);
$largeFiles = 0;
$notWritableFiles = 0;
@@ -121,10 +121,11 @@ class MapHistory extends AbstractCron {
}
}
$execTime = microtime(true) - $timeStart;
$importCount = $total = $largeFiles;
$count = count($truncatedFileNames);
// Log ------------------------
$log = new \Log('cron_' . __FUNCTION__ . '.log');
$log->write(sprintf(self::LOG_TEXT, __FUNCTION__, $largeFiles, implode(', ', $truncatedFileNames), $notWritableFiles, $readErrors, $writeErrors, $renameErrors, $deleteErrors, $execTime));
// Log --------------------------------------------------------------------------------------------------------
$text = sprintf(self::LOG_TEXT, $largeFiles, implode(', ', $truncatedFileNames), $notWritableFiles, $readErrors, $writeErrors, $renameErrors, $deleteErrors);
$this->logEnd(__FUNCTION__, $total, $count, $importCount, 0, $text);
}
}

View File

@@ -14,9 +14,14 @@ use Model\Pathfinder;
class MapUpdate extends AbstractCron {
const LOG_TEXT_MAPS = '%s (%d maps)';
/**
* log text
*/
const LOG_TEXT_MAPS_DELETED = ', %3s maps deleted';
// disabled maps will be fully deleted after (x) days
/**
* disabled maps will be fully deleted after (x) days
*/
const DAYS_UNTIL_MAP_DELETION = 30;
/**
@@ -25,7 +30,7 @@ class MapUpdate extends AbstractCron {
* @param \Base $f3
*/
function deactivateMapData(\Base $f3){
$this->setMaxExecutionTime();
$this->logStart(__FUNCTION__, false);
$privateMapLifetime = (int)Config::getMapsDefaultConfig('private.lifetime');
if($privateMapLifetime > 0){
@@ -40,6 +45,8 @@ class MapUpdate extends AbstractCron {
$pfDB->exec($sqlDeactivateExpiredMaps, ['lifetime' => $privateMapLifetime]);
}
}
$this->logEnd(__FUNCTION__);
}
/**
@@ -49,8 +56,8 @@ class MapUpdate extends AbstractCron {
* @throws \Exception
*/
function deleteMapData(\Base $f3){
$this->setMaxExecutionTime();
$deletedMapsCount = 0;
$this->logStart(__FUNCTION__);
$total = 0;
if($pfDB = $f3->DB->getDB('PF')){
$sqlDeleteDisabledMaps = "SELECT
@@ -63,11 +70,11 @@ class MapUpdate extends AbstractCron {
$disabledMaps = $pfDB->exec($sqlDeleteDisabledMaps, ['deletion_time' => self::DAYS_UNTIL_MAP_DELETION]);
if($deletedMapsCount = $pfDB->count()){
if($total = $pfDB->count()){
$mapModel = Pathfinder\AbstractPathfinderModel::getNew('MapModel');
foreach($disabledMaps as $data){
$mapModel->getById( (int)$data['id'], 3, false );
if( !$mapModel->dry() ){
if($mapModel->valid()){
$mapModel->erase();
}
$mapModel->reset();
@@ -75,9 +82,11 @@ class MapUpdate extends AbstractCron {
}
}
// Log ------------------------
$log = new \Log('cron_' . __FUNCTION__ . '.log');
$log->write( sprintf(self::LOG_TEXT_MAPS, __FUNCTION__, $deletedMapsCount) );
$count = $importCount = $total;
// Log --------------------------------------------------------------------------------------------------------
$text = sprintf(self::LOG_TEXT_MAPS_DELETED, $total);
$this->logEnd(__FUNCTION__, $total, $count, $importCount, 0, $text);
}
/**
@@ -87,9 +96,11 @@ class MapUpdate extends AbstractCron {
* @throws \Exception
*/
function deleteEolConnections(\Base $f3){
$this->setMaxExecutionTime();
$this->logStart(__FUNCTION__, false);
$eolExpire = (int)$f3->get('PATHFINDER.CACHE.EXPIRE_CONNECTIONS_EOL');
$total = 0;
$count = 0;
if($eolExpire > 0){
if($pfDB = $f3->DB->getDB('PF')){
$sql = "SELECT
@@ -109,19 +120,25 @@ class MapUpdate extends AbstractCron {
]);
if($connectionsData){
$total = count($connectionsData);
/**
* @var $connection Pathfinder\ConnectionModel
*/
$connection = Pathfinder\AbstractPathfinderModel::getNew('ConnectionModel');
foreach($connectionsData as $data){
$connection->getById( (int)$data['id'] );
if( !$connection->dry() ){
if($connection->valid()){
$connection->erase();
$count++;
}
}
}
}
}
$importCount = $total;
$this->logEnd(__FUNCTION__, $total, $count, $importCount);
}
/**
@@ -131,7 +148,11 @@ class MapUpdate extends AbstractCron {
* @throws \Exception
*/
function deleteExpiredConnections(\Base $f3){
$this->setMaxExecutionTime();
$this->logStart(__FUNCTION__, false);
$total = 0;
$count = 0;
$whExpire = (int)$f3->get('PATHFINDER.CACHE.EXPIRE_CONNECTIONS_WH');
if($whExpire > 0){
@@ -155,19 +176,25 @@ class MapUpdate extends AbstractCron {
]);
if($connectionsData){
$total = count($connectionsData);
/**
* @var $connection Pathfinder\ConnectionModel
*/
$connection = Pathfinder\AbstractPathfinderModel::getNew('ConnectionModel');
foreach($connectionsData as $data){
$connection->getById( (int)$data['id'] );
if( !$connection->dry() ){
if($connection->valid()){
$connection->erase();
$count++;
}
}
}
}
}
$importCount = $total;
$this->logEnd(__FUNCTION__, $total, $count, $importCount);
}
/**
@@ -176,9 +203,10 @@ class MapUpdate extends AbstractCron {
* @param \Base $f3
*/
function deleteSignatures(\Base $f3){
$this->setMaxExecutionTime();
$this->logStart(__FUNCTION__, false);
$signatureExpire = (int)$f3->get('PATHFINDER.CACHE.EXPIRE_SIGNATURES');
$count = 0;
if($signatureExpire > 0){
if($pfDB = $f3->DB->getDB('PF')){
$sqlDeleteExpiredSignatures = "DELETE `sigs` FROM
@@ -190,9 +218,13 @@ class MapUpdate extends AbstractCron {
TIMESTAMPDIFF(SECOND, `sigs`.`updated`, NOW() ) > :lifetime
";
$pfDB->exec($sqlDeleteExpiredSignatures, ['lifetime' => $signatureExpire]);
$count = $pfDB->exec($sqlDeleteExpiredSignatures, ['lifetime' => $signatureExpire]);
}
}
$importCount = $total = $count;
$this->logEnd(__FUNCTION__, $total, $count, $importCount);
}
}

View File

@@ -10,7 +10,10 @@ namespace cron;
class StatisticsUpdate extends AbstractCron {
const LOG_TEXT_STATISTICS = '%s (%d rows)';
/**
* log text
*/
const LOG_TEXT_STATISTICS = ', %5s rows deleted';
/**
* delete old statistics
@@ -19,7 +22,7 @@ class StatisticsUpdate extends AbstractCron {
* @param \Base $f3
*/
function deleteStatisticsData(\Base $f3){
$this->setMaxExecutionTime();
$this->logStart(__FUNCTION__);
$currentYear = (int)date('o');
$currentWeek = (int)date('W');
@@ -40,8 +43,10 @@ class StatisticsUpdate extends AbstractCron {
$deletedLogsCount = $pfDB->count();
// Log ------------------------
$log = new \Log('cron_' . __FUNCTION__ . '.log');
$log->write( sprintf(self::LOG_TEXT_STATISTICS, __FUNCTION__, $deletedLogsCount) );
// Log --------------------------------------------------------------------------------------------------------
$total = $count = $importCount = $deletedLogsCount;
$text = sprintf(self::LOG_TEXT_STATISTICS, $deletedLogsCount);
$this->logEnd(__FUNCTION__, $total, $count, $importCount, 0, $text);
}
}

View File

@@ -12,9 +12,15 @@ use Model;
class Universe extends AbstractCron {
/**
* log text
*/
const LOG_TEXT = '%s type: %s %s/%s peak: %s total: %s msg: %s';
const LOG_TEXT_SOV_FW = '%s %4s/%-4s checked, %s peak, %s total, %4s updated [%4s sovChanges, %4s fwChanges], msg: %s';
/**
* log text
*/
const LOG_TEXT_SOV_FW = ', %4s changes → [%4s sovChanges, %4s fwChanges], msg: %s';
/**
* format counter for output (min length)
@@ -292,7 +298,9 @@ class Universe extends AbstractCron {
* @throws \Exception
*/
function updateSovereigntyData(\Base $f3){
$this->setMaxExecutionTime();
$this->logStart(__FUNCTION__);
$params = $this->getParams(__FUNCTION__);
$timeTotalStart = microtime(true);
$msg = '';
@@ -306,13 +314,15 @@ class Universe extends AbstractCron {
$fwSystems = $fwSystems['systems'];
$ids = !empty($sovData = $sovData['map']) ? array_keys($sovData): [];
sort($ids, SORT_NUMERIC);
$total = count($ids);
$offset = ($params['offset'] > 0 && $params['offset'] < $total) ? $params['offset'] : 0;
$ids = array_slice($ids, $offset, $params['length']);
$importCount = count($ids);
$count = 0;
$changes = [];
foreach($ids as $id){
$count++;
// skip wormhole systems -> can not have sov data
// -> even though they are returned from sovereignty/map endpoint?!
if(
@@ -336,11 +346,12 @@ class Universe extends AbstractCron {
}
$system->reset();
$count++;
// stop loop if runtime gets close to "max_execution_time"
// -> we need some time for writing *.log file
if(!$this->isExecutionTimeLeft($timeTotalStart)){
$msg = 'Script execution stopped due to "max_execution_time" limit reached';
// TODO store current loop index and use it as new "offset" for next call
break;
}
}
@@ -349,13 +360,12 @@ class Universe extends AbstractCron {
return array_unique(array_merge($reducedIds, $changedIds));
}, []);
// Log ------------------------
$log = new \Log('cron_' . __FUNCTION__ . '.log');
$log->write(sprintf(self::LOG_TEXT_SOV_FW, __FUNCTION__,
$count, $importCount, $this->formatMemoryValue(memory_get_peak_usage ()),
$this->formatSeconds(microtime(true) - $timeTotalStart),
// Log --------------------------------------------------------------------------------------------------------
$text = sprintf(self::LOG_TEXT_SOV_FW,
count($changedIds), count($changes['sovereignty'] ? : []), count($changes['factionWarfare'] ? : []),
$msg));
$msg);
$this->logEnd(__FUNCTION__, $total, $count, $importCount, $offset, $text);
}
/**
@@ -366,7 +376,7 @@ class Universe extends AbstractCron {
* @throws \Exception
*/
function updateUniverseSystems(\Base $f3){
$this->setMaxExecutionTime();
$this->logStart(__FUNCTION__);
/**
* @var $systemModel Model\Universe\SystemModel
* @var $system Model\Universe\SystemModel
@@ -378,6 +388,8 @@ class Universe extends AbstractCron {
$system->updateModel();
}
}
$this->logEnd(__FUNCTION__);
}
}

155
app/main/lib/Cron.php Normal file
View File

@@ -0,0 +1,155 @@
<?php
namespace lib;
use Model\Pathfinder;
/**
* Class Cron
* @package lib
*
* @property array $jobs
*/
class Cron extends \Cron {
/**
* execution time buffer in percent
* -> cronJobs that exceed avg. exec. time + DEFAULT_BUFFER_EXEC_TIME show warnings
*/
const DEFAULT_BUFFER_EXEC_TIME = 20;
/**
* execution memory buffer in percent
* -> cronJobs that exceed avg. mem. peak + DEFAULT_BUFFER_MEM_PEAK show warnings
*/
const DEFAULT_BUFFER_MEM_PEAK = 20;
/**
* extends parent::isDue()
* -> adds check for "paused" jobs
* @param string $job
* @param int $time
* @return bool
*/
public function isDue($job, $time){
if($isDue = parent::isDue($job, $time)){
// check if job is not paused
if($job = $this->getJob($job)){
if($job->valid() && $job->isPaused){
$isDue = false;
}
}
}
return $isDue;
}
public function execute($job, $async = true) {
return parent::execute($job, $async);
}
/**
* @param $name
* @return string
*/
public function __get($name){
if(in_array($name, ['jobs'])){
return $this->$name;
}else{
return parent::__get($name);
}
}
/**
* @param array $jobConf
* @return array
*/
public function getJobDataFromConf(array $jobConf) : array {
return ['handler' => $jobConf[0], 'expr' => $jobConf[1]];
}
/**
* get all configured cronjobs (read from cron.ini)
* @param array $names
* @return array
*/
public function getJobsConfig(array $names = []) : array {
$config = [];
$jobs = array_filter($this->jobs, function(string $name) use ($names) : bool {
return !empty($names) ? in_array($name, $names) : true;
}, ARRAY_FILTER_USE_KEY );
foreach($jobs as $name => $jobConf){
$jobConf = $this->getJobDataFromConf($jobConf);
if($job = $this->registerJob($name, $jobConf)){
// get job config from DB
$config[$name] = $job->getData();
}else{
// job registration failed (e.g. DB connect failed) -> return min config from cron.ini
$jobConf = (object)$jobConf;
$jobConf->status = ['dbError' => Pathfinder\CronModel::STATUS['dbError']];
$jobConf->history = [];
$config[$name] = $jobConf;
}
$config[$name]->exprPreset = $this->checkPreset($config[$name]->expr);
}
ksort($config);
return $config;
}
/**
* @param string $name
* @param array $jobConf
* @return mixed|void
*/
public function registerJob(string $name, array $jobConf){
if($job = $this->getJob($name)){
if($job->dry()){
$job->name = $name;
}
$job->setData($jobConf);
return $job->save();
}
}
/**
* find CronModel by job $name
* @param string $name
* @return Pathfinder\CronModel|null
*/
public function getJob(string $name) : ?Pathfinder\CronModel {
$job = null;
try{
/**
* @var $job Pathfinder\CronModel
*/
$jobModel = Pathfinder\AbstractPathfinderModel::getNew('CronModel');
// we need to check if table exists here
// if not we get an error for later insert/update SQL actions
// -> e.g. if job is triggered manually on CLI
if($jobModel->tableExists()){
$jobModel->getByForeignKey('name', $name);
$job = $jobModel;
}
}catch(\Exception $e){
// Cron DB table not exists or other DB issues...
}
return $job;
}
/**
* check expression for a preset
* @param string $expr
* @return bool
*/
protected function checkPreset(string $expr){
if(preg_match('/^@(\w+)$/', $expr,$m)){
if(!isset($this->presets[$m[1]]))
return false;
return $this->presets[$m[1]];
}
return false;
}
}

View File

@@ -62,6 +62,32 @@ class SQL extends \DB\SQL {
return $count;
}
/**
* @param string|null $table
* @return array|null
*/
public function getTableStatus(?string $table) : ?array {
$status = null;
$sql = "SHOW TABLE STATUS";
$args = null;
if(!empty($table)){
$sql .= " LIKE :table";
$args = [
':table' => $table
];
}
if(!empty($statusRes = $this->exec($sql, $args))){
if(!empty($table)){
$status = reset($statusRes);
}else{
$status = $statusRes;
}
}
return $status;
}
/**
* set some default config for this DB
* @param string $characterSetDatabase

View File

@@ -0,0 +1,58 @@
<?php
namespace lib\format;
use lib\Config;
class Image extends \Prefab {
/**
* default image parameter for CCPs image server
* @see https://developers.eveonline.com/blog/article/from-image-server-to-a-whole-new-image-service-1
*/
const DEFAULT_EVE_SRC_CONFIG = [
'alliances' => [
'variant' => 'logo',
'size' => 64 // 64 is less 'blurry' with CSS downscale to 32 than native 32
],
'corporations' => [
'variant' => 'logo',
'size' => 64 // 64 is less 'blurry' with CSS downscale to 32 than native 32
],
'characters' => [
'variant' => 'portrait',
'size' => 32 // 32 is fine here, no visual difference to 64
],
'types' => [
'variant' => 'icon', // 'render' also works, 64px size is max for 'icon'
'size' => 64 // 64 is less 'blurry' with CSS downscale to 32 than native 32
]
];
/**
* build image server src URL
* @param string $resourceType
* @param int $resourceId
* @param int|null $size
* @param string|null $resourceVariant
* @return string|null
*/
public function eveSrcUrl(string $resourceType, int $resourceId, ?int $size = null, ?string $resourceVariant = null) : ?string {
$url = null;
if(
$resourceId &&
($serviceUrl = rtrim(Config::getPathfinderData('api.ccp_image_server'), '/')) &&
($defaults = static::DEFAULT_EVE_SRC_CONFIG[$resourceType])
){
$parts = [$serviceUrl, $resourceType, $resourceId, $resourceVariant ? : $defaults['variant']];
$url = implode('/', $parts);
$params = ['size' => $size ? : $defaults['size']];
$url .= '?' . http_build_query($params);
}
return $url;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace lib\format;
class Number extends \Prefab {
/**
* convert Bytes to string + suffix
* @param int $bytes
* @param int $precision
* @return string
*/
public function bytesToString($bytes,$precision = 2) : string {
$result = '0';
if($bytes){
$base = log($bytes, 1024);
$suffixes = ['', 'KB', 'M', 'GB', 'TB'];
$result = round(pow(1024, $base - floor($base)), $precision) .''. $suffixes[(int)floor($base)];
}
return $result;
}
}

View File

@@ -25,7 +25,8 @@ class Resource extends \Prefab {
'script' => 'script',
'font' => 'font',
'document' => 'document',
'image' => 'image'
'image' => 'image',
'url' => ''
];
/**
@@ -51,7 +52,8 @@ class Resource extends \Prefab {
'script' => '',
'font' => '',
'document' => '',
'image' => ''
'image' => '',
'url' => ''
];
/**
@@ -83,12 +85,13 @@ class Resource extends \Prefab {
private $resources = [];
/**
* set option
* set or extend option
* @param string $option
* @param $value
* @param bool $extend
*/
public function setOption(string $option, $value){
$this->$option = $value;
public function setOption(string $option, $value, bool $extend = false){
$this->$option = ($extend && is_array($value) && is_array($this->$option)) ? array_merge($this->$option, $value) : $value;
}
/**
@@ -117,7 +120,8 @@ class Resource extends \Prefab {
* @return string
*/
public function getLink(string $group, string $file) : string {
$link = $this->getPath($group) . '/' . $file;
// $group 'url' expect full qualified URLs
$link = ($group == 'url' ? '' : $this->getPath($group) . '/') . $file;
// add extension if not already part of the file
// -> allows switching between extensions (e.g. .jpg, .png) for the same image
$link .= empty(pathinfo($file, PATHINFO_EXTENSION)) ? '.' . $this->getFileExtension($group) : '';
@@ -191,8 +195,8 @@ class Resource extends \Prefab {
if( empty($conf['options']['rel']) ){
$conf['options']['rel'] = self::ATTR_REL;
}
if( empty($conf['options']['as']) ){
$conf['options']['as'] = $group;
if( empty($conf['options']['as']) && !empty($attrAs = $this->getLinkAttrAs($group)) ){
$conf['options']['as'] = $attrAs;
}
if( empty($conf['options']['type']) && !empty($attrType = $this->getLinkAttrType($group)) ){
$conf['options']['type'] = $attrType;
@@ -202,7 +206,6 @@ class Resource extends \Prefab {
$conf['options'] = $conf['options'] + $additionalAttr;
}
}
unset($options); // unset ref
}
unset($resources); // unset ref
}
@@ -237,6 +240,12 @@ class Resource extends \Prefab {
return isset(self::ATTR_ADD[$group]) ? self::ATTR_ADD[$group] : [];
}
/**
* get file extension by $group
* -> e.g. or fonts
* @param string $group
* @return string
*/
protected function getFileExtension(string $group) : string {
return isset($this->fileExt[$group]) ? $this->fileExt[$group] : '';
}

View File

@@ -88,6 +88,12 @@ abstract class AbstractModel extends Cortex {
*/
protected $validationError = [];
/**
* default charset for table
*/
const DEFAULT_CHARSET = 'utf8mb4';
/**
* default caching time of field schema - seconds
*/
@@ -137,8 +143,9 @@ abstract class AbstractModel extends Cortex {
* @param null $table
* @param null $fluid
* @param int $ttl
* @param string $charset
*/
public function __construct($db = NULL, $table = NULL, $fluid = NULL, $ttl = self::DEFAULT_TTL){
public function __construct($db = null, $table = null, $fluid = null, $ttl = self::DEFAULT_TTL, $charset = self::DEFAULT_CHARSET){
if(!is_object($db)){
$db = self::getF3()->DB->getDB(static::DB_ALIAS);
@@ -149,6 +156,9 @@ abstract class AbstractModel extends Cortex {
self::getF3()->set('HALT', true);
}
// set charset -> used during table setup()
$this->charset = $charset;
$this->addStaticFieldConfig();
parent::__construct($db, $table, $fluid, $ttl);
@@ -181,6 +191,14 @@ abstract class AbstractModel extends Cortex {
});
}
/**
* checks whether table exists on DB
* @return bool
*/
public function tableExists() : bool {
return is_object($this->db) ? $this->db->tableExists($this->table) : false;
}
/**
* clear existing table Schema cache
* @return bool
@@ -912,7 +930,7 @@ abstract class AbstractModel extends Cortex {
*/
protected function isOutdated() : bool {
$outdated = true;
if(!$this->dry()){
if($this->valid()){
try{
$timezone = $this->getF3()->get('getTimeZone')();
$currentTime = new \DateTime('now', $timezone);

View File

@@ -12,23 +12,83 @@ use DB\SQL\Schema;
abstract class AbstractSystemApiBasicModel extends AbstractPathfinderModel {
public function __construct($db = NULL, $table = NULL, $fluid = NULL, $ttl = 0){
/**
* column count for data
*/
const DATA_COLUMN_COUNT = 24;
/**
* column name prefix for data columns
*/
const DATA_COLUMN_PREFIX = 'value';
/**
* AbstractSystemApiBasicModel constructor.
* @param null $db
* @param null $table
* @param null $fluid
* @param int $ttl
*/
public function __construct($db = null, $table = null, $fluid = null, $ttl = 0){
$this->addStaticKillFieldConfig();
parent::__construct($db, $table, $fluid, $ttl);
}
/**
* get data
* @return \stdClass
*/
public function getData(){
$data = (object)[];
$data->systemId = $this->getRaw('systemId');
$data->values = $this->getValues();
$data->updated = strtotime($this->updated);
return $data;
}
/**
* get all "valX" column data as array
* -> "start" (most recent) value is stored in column name stored in "lastUpdatedValue" column
* @return array
*/
protected function getValues() : array {
$valueColumnNames = range(1, static::DATA_COLUMN_COUNT);
$preFixer = function(&$value, $key, $prefix){
$value = $prefix . $value;
};
array_walk($valueColumnNames, $preFixer, static::DATA_COLUMN_PREFIX);
$valueColumns = array_intersect_key($this->cast(null, 0), array_flip($valueColumnNames));
$lastUpdatedValue = $this->lastUpdatedValue ? : 1;
// bring values in correct order based on "lastUpdatedValue"
$valueColumnsA = array_slice($valueColumns, $lastUpdatedValue - static::DATA_COLUMN_COUNT , null, true);
$valueColumnsB = array_slice($valueColumns, 0, $lastUpdatedValue, true);
return array_values($valueColumnsA + $valueColumnsB);
}
/**
* extent the fieldConf Array with static fields for each table
*/
private function addStaticKillFieldConfig(){
if(is_array($this->fieldConf)){
$staticFieldConfig = [];
for($i = 1; $i <= 24; $i++){
$staticFieldConfig['value' . $i] = [
$staticFieldConfig['lastUpdatedValue'] = [
'type' => Schema::DT_INT,
'nullable' => false,
'default' => 1,
'index' => true
];
for($i = 1; $i <= static::DATA_COLUMN_COUNT; $i++){
$staticFieldConfig[static::DATA_COLUMN_PREFIX . $i] = [
'type' => Schema::DT_INT,
'nullable' => false,
'default' => 0,
'default' => 0
];
}

View File

@@ -1243,7 +1243,8 @@ class CharacterModel extends AbstractPathfinderModel {
$skipRest = false;
$logHistoryData = $this->filterLogsHistory(function(array $historyEntry) use ($mapId, $systemId, &$skipRest) : bool {
$addEntry = false;
if(in_array($mapId, (array)$historyEntry['mapIds'], true)){
//if(in_array($mapId, (array)$historyEntry['mapIds'], true)){ // $historyEntry is checked by EACH map -> would auto add system on map switch! #827
if(!empty((array)$historyEntry['mapIds'])){ // if $historyEntry was already checked by ANY other map -> no further checks
$skipRest = true;
}

View File

@@ -0,0 +1,287 @@
<?php
namespace Model\Pathfinder;
use DB\SQL\Schema;
class CronModel extends AbstractPathfinderModel {
/**
* @var string
*/
protected $table = 'cron';
const STATUS = [
'unknown' => [
'type' => 'warning',
'icon' => 'question',
'msg' => 'No status information available'
],
'dbError' => [
'type' => 'warning',
'icon' => 'fa-exclamation-triangle',
'msg' => 'Failed to sync job data with DB'
],
'notExecuted' => [
'type' => 'hint',
'icon' => 'fa-bolt',
'msg' => 'Has not been executed'
],
'notFinished' => [
'type' => 'danger',
'icon' => 'fa-clock',
'msg' => 'Not finished within max exec. time'
],
'inProgress' => [
'type' => 'success',
'icon' => 'fa-play',
'msg' => 'Started. In execution…'
],
'isPaused' => [
'type' => 'warning',
'icon' => 'fa-pause',
'msg' => 'Paused. No execution on next time trigger (skip further execution)'
],
'onHold' => [
'type' => 'information',
'icon' => 'fa-history fa-flip-horizontal',
'msg' => 'Is active. Waiting for next trigger…'
]
];
/**
* @var array
*/
protected $fieldConf = [
'name' => [
'type' => Schema::DT_VARCHAR128,
'nullable' => false,
'default' => '',
'index' => true,
'unique' => true,
'validate' => 'notEmpty'
],
'handler' => [
'type' => Schema::DT_VARCHAR256,
'nullable' => false,
'default' => '',
'index' => true,
'unique' => true,
'validate' => 'notEmpty'
],
'expr' => [
'type' => Schema::DT_VARCHAR128,
'nullable' => false,
'default' => '',
'validate' => 'notEmpty'
],
'isPaused' => [
'type' => Schema::DT_BOOL,
'nullable' => false,
'default' => 0
],
'lastExecStart' => [
'type' => Schema::DT_DOUBLE,
'nullable' => true,
'default' => null
],
'lastExecEnd' => [
'type' => Schema::DT_DOUBLE,
'nullable' => true,
'default' => null
],
'lastExecMemPeak' => [
'type' => Schema::DT_FLOAT,
'nullable' => true,
'default' => null
],
'lastExecState' => [
'type' => self::DT_JSON
],
'history' => [
'type' => self::DT_JSON
]
];
/**
* set data by associative array
* @param array $data
*/
public function setData(array $data){
$this->copyfrom($data, ['handler', 'expr', 'lastExecStart', 'lastExecEnd', 'lastExecMemPeak', 'lastExecState']);
}
/**
* get data
* @return object
*/
public function getData(){
$data = (object) [];
$data->id = $this->_id;
$data->name = $this->name;
$data->handler = $this->handler;
$data->expr = $this->expr;
$data->logFile = $this->logFileExists();
$data->lastExecStart = $this->lastExecStart;
$data->lastExecEnd = $this->lastExecEnd;
$data->lastExecMemPeak = $this->lastExecMemPeak;
$data->lastExecDuration = $this->getExecDuration();
$data->lastExecState = $this->lastExecState;
$data->isPaused = $this->isPaused;
$data->status = $this->getStatus();
$data->history = $this->getHistory(true);
return $data;
}
/**
* setter for system alias
* @param string $lastExecStart
* @return string
*/
public function set_lastExecStart($lastExecStart){
$this->logState();
return $lastExecStart;
}
/**
* log execution "state" for prev run in 'history' column
*/
protected function logState(){
$this->history = $this->getHistory() ? : null;
// reset data from last run
$this->lastExecEnd = null;
$this->lastExecMemPeak = null;
}
/**
* @param bool $addLastIfFinished
* @return array
*/
protected function getHistory(bool $addLastIfFinished = false) : array {
$history = $this->history ? : [];
if(!is_null($this->lastExecStart)){
if(!$addLastIfFinished || !is_null($this->lastExecEnd)){
array_unshift($history, [
'lastExecStart' => $this->lastExecStart,
'lastExecMemPeak' => $this->lastExecMemPeak,
'lastExecDuration' => (!$this->inExec() && !$this->isTimedOut()) ? $this->getExecDuration() : 0,
'status' => array_intersect(array_keys($this->getStatus()), ['inProgress', 'notFinished'])
]);
$history = array_slice($history, 0, 10);
}
}
return $history;
}
/**
* get current job status based on its current data
* @return array
*/
protected function getStatus() : array {
$status = [];
if($this->isPaused){
$status['isPaused'] = self::STATUS['isPaused'];
}
if($this->inExec() && !$this->isTimedOut()){
$status['inProgress'] = self::STATUS['inProgress'];
}
if(empty($status)){
$status['onHold'] = self::STATUS['onHold'];
}
if($this->isTimedOut()){
$status['notFinished'] = self::STATUS['notFinished'];
}
if(!$this->lastExecStart){
$status['notExecuted'] = self::STATUS['notExecuted'];
}
return empty($status) ? ['unknown' => self::STATUS['unknown']] : array_reverse($status);
}
/**
* based on the data on DB, job is marked at "in progress"
* @return bool
*/
protected function inExec() : bool {
return $this->lastExecStart && !$this->lastExecEnd;
}
/**
* @return bool
* @throws \Exception
*/
protected function isTimedOut() : bool {
$timedOut = false;
if($this->lastExecStart){
$timezone = self::getF3()->get('getTimeZone')();
$startTime = \DateTime::createFromFormat(
'U.u',
number_format($this->lastExecStart, 6, '.', ''),
$timezone
);
$timeBuffer = 60 * 60;
$startTime->add(new \DateInterval('PT' . $timeBuffer . 'S'));
if($this->lastExecEnd){
$endTime = \DateTime::createFromFormat(
'U.u',
number_format($this->lastExecEnd, 6, '.', ''),
$timezone
);
}else{
$endTime = new \DateTime('now', $timezone);
}
$timedOut = $startTime < $endTime;
}
return $timedOut;
}
/**
* @return float|null
*/
protected function getExecDuration() : ?float {
$duration = null;
if($this->lastExecStart && $this->lastExecEnd){
$duration = (float)$this->lastExecEnd - (float)$this->lastExecStart;
}
return $duration;
}
/**
* extract function name from $this->handler
* -> it is used for the log file name
* @return string|null
*/
protected function getLogFileName() : ?string {
return ($this->handler && preg_match('/^.*->(\w+)$/', $this->handler,$m)) ? 'cron_' . $m[1] . '.log' : null;
}
/**
* checks whether a log file exists for this cronjob
* -> will be created after job execution
* @return string
*/
protected function logFileExists() : ?string {
$filePath = null;
if($file = $this->getLogFileName()){
$filePath = is_file($path = self::getF3()->get('LOGS') . $file) ? $path : null;
}
return $filePath;
}
}

View File

@@ -134,11 +134,12 @@ class StructureModel extends AbstractPathfinderModel {
if($corporationId){
if($corporationId !== $oldCorporationId){
// make sure there is already corporation data available for new corporationId
// -> $ttl = 0 is important! Otherwise "bulk" update for structures could fail
/**
* @var CorporationModel $corporation
*/
$corporation = $this->rel('corporationId');
$corporation->getById($corporationId);
$corporation->getById($corporationId, 0);
if($corporation->dry()){
$corporationId = null;
}
@@ -234,7 +235,7 @@ class StructureModel extends AbstractPathfinderModel {
* @param int $systemId
*/
public function getByName(CorporationModel $corporation, string $name, int $systemId){
if( !$corporation->dry() && $name){
if($corporation->valid() && $name){
$this->has('structureCorporations', ['corporationId = :corporationId', ':corporationId' => $corporation->_id]);
$this->load(['name = :name AND systemId = :systemId AND active = :active',
':name' => $name,

View File

@@ -12,8 +12,14 @@ use DB\SQL\Schema;
class SystemFactionKillModel extends AbstractSystemApiBasicModel {
/**
* @var string
*/
protected $table = 'system_kills_factions';
/**
* @var array
*/
protected $fieldConf = [
'active' => [
'type' => Schema::DT_BOOL,

View File

@@ -12,8 +12,14 @@ use DB\SQL\Schema;
class SystemJumpModel extends AbstractSystemApiBasicModel {
/**
* @var string
*/
protected $table = 'system_jumps';
/**
* @var array
*/
protected $fieldConf = [
'active' => [
'type' => Schema::DT_BOOL,

View File

@@ -12,8 +12,14 @@ use DB\SQL\Schema;
class SystemPodKillModel extends AbstractSystemApiBasicModel {
/**
* @var string
*/
protected $table = 'system_kills_pods';
/**
* @var array
*/
protected $fieldConf = [
'active' => [
'type' => Schema::DT_BOOL,

View File

@@ -12,8 +12,14 @@ use DB\SQL\Schema;
class SystemShipKillModel extends AbstractSystemApiBasicModel {
/**
* @var string
*/
protected $table = 'system_kills_ships';
/**
* @var array
*/
protected $fieldConf = [
'active' => [
'type' => Schema::DT_BOOL,

View File

@@ -167,7 +167,7 @@ class SystemSignatureModel extends AbstractMapTrackingModel {
*/
protected function validate_name(string $key, string $val): bool {
$valid = true;
if(mb_strlen($val) < 3){
if(!mb_ereg('^[a-zA-Z]{3}-\d{3}$', $val)){
$valid = false;
$this->throwValidationException($key);
}

View File

@@ -143,7 +143,7 @@ class SystemModel extends AbstractUniverseModel {
// 'Shattered' systems have ONLY planets named with '(shattered)'
// -> system 'Thera' has '(shattered)' AND other planets -> not shattered.
// -> system 'J164104, 'J115422' - the only non-shattered wormholes which have a shattered planet -> not shattered.
$data->shattered = count(array_filter($planetsData, function(object $planetData){
$data->shattered = count(array_filter($planetsData, function($planetData){
return property_exists($planetData, 'type') &&
(strpos(strtolower($planetData->type->name), '(shattered)') !== false);
})) == count($planetsData);

View File

@@ -13,8 +13,8 @@ NAME = Pathfinder
; Version is used for CSS/JS cache busting and is part of the URL for static resources:
; e.g. public/js/vX.X.X/app.js
; Syntax: String (current version)
; Default: v1.5.4
VERSION = v1.5.4
; Default: v1.5.5
VERSION = v1.5.5
; Contact information [optional]
; Shown on 'licence', 'contact' page.
@@ -375,7 +375,7 @@ LOG_LINES = 1000
; API =============================================================================================
[PATHFINDER.API]
CCP_IMAGE_SERVER = https://image.eveonline.com
CCP_IMAGE_SERVER = https://images.evetech.net
Z_KILLBOARD = https://zkillboard.com/api
EVEEYE = https://eveeye.com
DOTLAN = http://evemaps.dotlan.net

View File

@@ -80,5 +80,9 @@ MAXMEMORY_POLICY = allkeys-lru
NODE = 6.0
NPM = 3.10.0
[REQUIREMENTS.CRON]
CLI = 1
LOG = 1
[REQUIREMENTS.DATA]
NEIGHBOURS = 5201

View File

@@ -15,6 +15,6 @@ $f3->config('app/config.ini', true);
lib\Config::instance($f3);
// initiate cron-jobs
Cron::instance();
lib\Cron::instance();
$f3->run();

View File

@@ -37,6 +37,7 @@ requirejs.config({
slidebars: 'lib/slidebars', // v2.0.2 Slidebars - side menu plugin https://www.adchsm.com/slidebars/
jsPlumb: 'lib/jsplumb', // v2.9.3 jsPlumb main map draw plugin http://jsplumb.github.io/jsplumb/home.html
farahey: 'lib/farahey', // v1.1.2 jsPlumb "magnetizing" plugin extension - https://github.com/ThomasChan/farahey
easyTimer: 'lib/easytimer.min', // v4.0.2 EasyTimer - Timer/Chronometer/Countdown library - http://albert-gonzalez.github.io/easytimer.js
customScrollbar: 'lib/jquery.mCustomScrollbar.min', // v3.1.5 Custom scroll bars - http://manos.malihu.gr
mousewheel: 'lib/jquery.mousewheel.min', // v3.1.13 Mousewheel - https://github.com/jquery/jquery-mousewheel
xEditable: 'lib/bootstrap-editable.min', // v1.5.1 X-editable - in placed editing
@@ -44,8 +45,7 @@ requirejs.config({
raphael: 'lib/raphael.min', // v2.2.8 Raphaël - required for morris - https://dmitrybaranovskiy.github.io/raphael
bootbox: 'lib/bootbox.min', // v5.2.0 Bootbox.js - custom dialogs - http://bootboxjs.com
easyPieChart: 'lib/jquery.easypiechart.min', // v2.1.6 Easy Pie Chart - HTML 5 pie charts - http://rendro.github.io/easy-pie-chart
peityInlineChart: 'lib/jquery.peity.min', // v3.2.1 Inline Chart - http://benpickles.github.io/peity/
dragToSelect: 'lib/jquery.dragToSelect', // v1.1 Drag to Select - http://andreaslagerkvist.com/jquery/drag-to-select
peityInlineChart: 'lib/jquery.peity.min', // v3.3.0 Inline Chart - http://benpickles.github.io/peity/
hoverIntent: 'lib/jquery.hoverIntent.min', // v1.10.0 Hover intention - http://cherne.net/brian/resources/jquery.hoverIntent.html
select2: 'lib/select2.min', // v4.0.3 Drop Down customization - https://select2.github.io
validator: 'lib/validator.min', // v0.10.1 Validator for Bootstrap 3 - https://github.com/1000hz/bootstrap-validator
@@ -53,7 +53,7 @@ requirejs.config({
blueImpGallery: 'lib/blueimp-gallery', // v2.21.3 Image Gallery - https://github.com/blueimp/Gallery
blueImpGalleryHelper: 'lib/blueimp-helper', // helper function for Blue Imp Gallery
blueImpGalleryBootstrap: 'lib/bootstrap-image-gallery', // v3.4.2 Bootstrap extension for Blue Imp Gallery - https://blueimp.github.io/Bootstrap-Image-Gallery
bootstrapConfirmation: 'lib/bootstrap-confirmation', // v1.0.5 Bootstrap extension for inline confirm dialog - https://github.com/tavicu/bs-confirmation
bootstrapConfirmation: 'lib/bootstrap-confirmation.min', // v1.0.7 Bootstrap extension for inline confirm dialog - https://github.com/tavicu/bs-confirmation
bootstrapToggle: 'lib/bootstrap-toggle.min', // v2.2.0 Bootstrap Toggle (Checkbox) - http://www.bootstraptoggle.com
lazyload: 'lib/jquery.lazyload.min', // v1.9.7 LazyLoader images - https://appelsiini.net/projects/lazyload/
sortable: 'lib/sortable.min', // v1.6.0 Sortable - drag&drop reorder - https://github.com/rubaxa/Sortable
@@ -147,9 +147,6 @@ requirejs.config({
peityInlineChart: {
deps: ['jquery']
},
dragToSelect: {
deps: ['jquery']
},
hoverIntent: {
deps: ['jquery']
},

View File

@@ -1,21 +1,118 @@
/**
* Created by exodus4d on 06.07.2015.
* Created by exodus4d
* static signature types
*
* (*) marked fields are in-game verified and
* proofed, signature names (copy & paste from scanning window)
*/
define(['jquery'], ($) => {
define([], () => {
'use strict';
// signature sources
// http://de.sistersprobe.wikia.com/wiki/EVE_Sister_Core_Scanner_Probe_Wiki
// Combat sites ===================================================================================================
let c1Combat = {
1: 'Perimeter Ambush Point',
2: 'Perimeter Camp',
3: 'Phase Catalyst Node',
4: 'The Line'
};
let c2Combat = {
1: 'Perimeter Checkpoint',
2: 'Perimeter Hangar',
3: 'The Ruins of Enclave Cohort 27',
4: 'Sleeper Data Sanctuary'
};
let c3Combat = {
1: 'Fortification Frontier Stronghold',
2: 'Outpost Frontier Stronghold',
3: 'Solar Cell',
4: 'The Oruze Construct'
};
let c4Combat = {
1: 'Frontier Barracks',
2: 'Frontier Command Post',
3: 'Integrated Terminus',
4: 'Sleeper Information Sanctum'
};
let c5Combat = {
1: 'Core Garrison', //*
2: 'Core Stronghold', //*
3: 'Oruze Osobnyk', //*
4: 'Quarantine Area'
};
let c6Combat = {
1: 'Core Citadel', //*
2: 'Core Bastion', //*
3: 'Strange Energy Readings', //*
4: 'The Mirror' //*
};
// Thera WH
let c12Combat = {
1: 'Epicenter',
2: 'Expedition Command Outpost Wreck',
3: 'Planetary Colonization Office Wreck',
4: 'Testing Facilities'
};
// Drifter Sentinel WH
let c14Combat = {
1: 'Monolith',
2: 'Wormhole in Rock Circle',
3: 'Opposing Spatial Rifts',
4: 'Sleeper Enclave Debris',
5: 'Crystal Resource'
};
// Drifter Barbican WH
let c15Combat = {
1: 'Wrecked Ships',
2: 'Unstable Wormhole',
3: 'Spatial Rift',
4: 'Heavily Guarded Spatial Rift',
5: 'Crystals'
};
// Drifter Vidette WH
let c16Combat = {
1: 'Ship Graveyard',
2: 'Sleeper Engineering Station',
3: 'Spatial Rift',
4: 'Sleeper Enclave in Coral Rock',
5: 'Crystals and Stone Circle'
};
// Drifter Conflux WH
let c17Combat = {
1: 'Monolith',
2: 'Caged Wormhole',
3: 'Rock Formation and Wormhole',
4: 'Particle Acceleration Array',
5: 'Guarded Asteroid Station'
};
// Drifter Redoubt WH
let c18Combat = {
1: 'Ship Graveyard',
2: 'Caged Wormhole',
3: 'Spatial Rift Generator',
4: 'Sleeper Enclave',
5: 'Hollow Asteroid'
};
// Relic sites ====================================================================================================
// NullSec Relic sites, which can also spawn in C1, C2, C3 wormholes
let nullSecRelicSites = {
let nullRelic = {
10: 'Ruined Angel Crystal Quarry',
11: 'Ruined Angel Monument Site',
12: 'Ruined Angel Science Outpost',
@@ -38,8 +135,40 @@ define(['jquery'], ($) => {
29: 'Ruined Serpentis Temple Site'
};
let c1Relic = Object.assign({}, nullRelic, {
1: 'Forgotten Perimeter Coronation Platform', //*
2: 'Forgotten Perimeter Power Array' //*
});
let c2Relic = Object.assign({}, nullRelic, {
1: 'Forgotten Perimeter Gateway', //*
2: 'Forgotten Perimeter Habitation Coils' //*
});
let c3Relic = Object.assign({}, nullRelic, {
1: 'Forgotten Frontier Quarantine Outpost', //*
2: 'Forgotten Frontier Recursive Depot' //*
});
let c4Relic = {
1: 'Forgotten Frontier Conversion Module',
2: 'Forgotten Frontier Evacuation Center'
};
let c5Relic = {
1: 'Forgotten Core Data Field',
2: 'Forgotten Core Information Pen'
};
let c6Relic = {
1: 'Forgotten Core Assembly Hall', //*
2: 'Forgotten Core Circuitry Disassembler' //*
};
// Data sites =====================================================================================================
// NulSec Data sites, which can also spawn in C1, C2, C3 wormholes
let nullSecDataSites = {
let nullData = {
10: 'Abandoned Research Complex DA005',
11: 'Abandoned Research Complex DA015',
12: 'Abandoned Research Complex DC007',
@@ -68,413 +197,390 @@ define(['jquery'], ($) => {
35: 'Central Serpentis Survey Site'
};
let c1Data = Object.assign({}, nullData, {
1: 'Unsecured Perimeter Amplifier', //*
2: 'Unsecured Perimeter Information Center' //*
});
let c2Data = Object.assign({}, nullData, {
1: 'Unsecured Perimeter Comms Relay', //*
2: 'Unsecured Perimeter Transponder Farm' //*
});
let c3Data = Object.assign({}, nullData, {
1: 'Unsecured Frontier Database', //*
2: 'Unsecured Frontier Receiver' //*
});
let c4Data = {
1: 'Unsecured Frontier Digital Nexus',
2: 'Unsecured Frontier Trinary Hub'
};
let c5Data = {
1: 'Unsecured Frontier Enclave Relay',
2: 'Unsecured Frontier Server Bank'
};
let c6Data = {
1: 'Unsecured Core Backup Array', //*
2: 'Unsecured Core Emergence' //*
};
// Gas sites ======================================================================================================
let c1Gas = {
1: 'Barren Perimeter Reservoir', //*
2: 'Token Perimeter Reservoir', //*
3: 'Minor Perimeter Reservoir', //*
4: 'Sizeable Perimeter Reservoir', //*
5: 'Ordinary Perimeter Reservoir' //*
};
let c2Gas = {
1: 'Barren Perimeter Reservoir', //*
2: 'Token Perimeter Reservoir', //*
3: 'Minor Perimeter Reservoir', //*
4: 'Sizeable Perimeter Reservoir', //*
5: 'Ordinary Perimeter Reservoir' //*
};
let c3Gas = {
1: 'Barren Perimeter Reservoir', //*
2: 'Token Perimeter Reservoir', //*
3: 'Minor Perimeter Reservoir', //*
4: 'Sizeable Perimeter Reservoir', //*
5: 'Ordinary Perimeter Reservoir', //*
6: 'Bountiful Frontier Reservoir', //*
7: 'Vast Frontier Reservoir' //*
};
let c4Gas = {
1: 'Barren Perimeter Reservoir', //*
2: 'Token Perimeter Reservoir', //*
3: 'Minor Perimeter Reservoir', //*
4: 'Sizeable Perimeter Reservoir', //*
5: 'Ordinary Perimeter Reservoir', //*
6: 'Vast Frontier Reservoir', //*
7: 'Bountiful Frontier Reservoir' //*
};
let c5Gas = {
1: 'Barren Perimeter Reservoir', //*
2: 'Minor Perimeter Reservoir', //*
3: 'Ordinary Perimeter Reservoir', //*
4: 'Sizeable Perimeter Reservoir', //*
5: 'Token Perimeter Reservoir', //*
6: 'Bountiful Frontier Reservoir', //*
7: 'Vast Frontier Reservoir', //*
8: 'Instrumental Core Reservoir', //*
9: 'Vital Core Reservoir' //*
};
let c6Gas = {
1: 'Barren Perimeter Reservoir', //*
2: 'Minor Perimeter Reservoir', //*
3: 'Ordinary Perimeter Reservoir', //*
4: 'Sizeable Perimeter Reservoir', //*
5: 'Token Perimeter Reservoir', //*
6: 'Bountiful Frontier Reservoir', //*
7: 'Vast Frontier Reservoir', //*
8: 'Instrumental Core Reservoir', //*
9: 'Vital Core Reservoir' //*
};
// Ore sites ======================================================================================================
let c1Ore = {
1: 'Ordinary Perimeter Deposit', //*
2: 'Common Perimeter Deposit', //*
3: 'Unexceptional Frontier Deposit', //*
4: 'Average Frontier Deposit', //*
5: 'Isolated Core Deposit', //*
6: 'Uncommon Core Deposit' //*
};
let c2Ore = {
1: 'Ordinary Perimeter Deposit', //*
2: 'Common Perimeter Deposit', //*
3: 'Unexceptional Frontier Deposit', //*
4: 'Average Frontier Deposit', //*
5: 'Isolated Core Deposit', //*
6: 'Uncommon Core Deposit' //*
};
let c3Ore = {
1: 'Ordinary Perimeter Deposit', //*
2: 'Common Perimeter Deposit', //*
3: 'Unexceptional Frontier Deposit', //*
4: 'Average Frontier Deposit', //*
5: 'Infrequent Core Deposit', //*
6: 'Unusual Core Deposit' //*
};
let c4Ore = {
1: 'Ordinary Perimeter Deposit', //*
2: 'Common Perimeter Deposit', //*
3: 'Unexceptional Frontier Deposit', //*
4: 'Average Frontier Deposit', //*
5: 'Unusual Core Deposit', //*
6: 'Infrequent Core Deposit' //*
};
let c5Ore = {
1: 'Average Frontier Deposit', //*
2: 'Unexceptional Frontier Deposit', //*
3: 'Uncommon Core Deposit', //*
4: 'Ordinary Perimeter Deposit', //*
5: 'Common Perimeter Deposit', //*
6: 'Exceptional Core Deposit', //*
7: 'Infrequent Core Deposit', //*
8: 'Unusual Core Deposit', //*
9: 'Rarified Core Deposit', //*
10: 'Isolated Core Deposit' //*
};
let c6Ore = {
1: 'Ordinary Perimeter Deposit', //*
2: 'Common Perimeter Deposit', //*
3: 'Unexceptional Frontier Deposit', //*
4: 'Average Frontier Deposit', //*
5: 'Rarified Core Deposit' //*
};
let c13Ore = {
1: 'Shattered Debris Field',
2: 'Shattered Ice Field'
};
// Wormholes ======================================================================================================
// all k-space exits are static or K162
let c1WH = {
1: 'H121 - C1',
2: 'C125 - C2',
3: 'O883 - C3',
4: 'M609 - C4',
5: 'L614 - C5',
6: 'S804 - C6',
7: 'F353 - C12 Thera'
};
// all w-space -> w-space are statics or K162
let c2WH = {
1: 'Z647 - C1',
2: 'D382 - C2',
3: 'O477 - C3',
4: 'Y683 - C4',
5: 'N062 - C5',
6: 'R474 - C6',
7: 'F135 - C12 Thera'
};
// all k-space exits are static or K162
let c3WH = {
1: 'V301 - C1',
2: 'I182 - C2',
3: 'N968 - C3',
4: 'T405 - C4',
5: 'N770 - C5',
6: 'A982 - C6',
7: 'F135 - C12 Thera'
};
// no *wandering* w-space -> w-space
// all holes are statics or K162
let c4WH = {
1: 'S047 - H',
2: 'N290 - L',
3: 'K329 - 0.0'
};
let c5WH = {
1: 'D792 - H',
2: 'C140 - L',
3: 'Z142 - 0.0'
};
let c6WH = {
1: 'B520 - H',
2: 'D792 - H',
3: 'C140 - L',
4: 'C391 - L',
5: 'C248 - 0.0',
6: 'Z142 - 0.0'
};
// Shattered WH (some of them are static)
let c13WH = {
1: 'P060 - C1',
2: 'Z647 - C1',
3: 'D382 - C2',
4: 'L005 - C2',
5: 'N766 - C2',
6: 'C247 - C3',
7: 'M267 - C3',
8: 'O477 - C3',
9: 'X877 - C4',
10: 'Y683 - C4',
11: 'H296 - C5',
12: 'H900 - C5',
13: 'H296 - C5',
14: 'N062 - C5', // ??
15: 'V911 - C5',
16: 'U574 - C6',
17: 'V753 - C6',
18: 'W237 - C6',
19: 'B274 - H',
20: 'D792 - H',
21: 'D845 - H',
22: 'N110 - H',
23: 'A239 - L',
24: 'C391 - L',
25: 'J244 - L',
26: 'U201 - L', // ??
27: 'U210 - L',
28: 'C248 - 0.0',
29: 'E545 - 0.0',
30: 'K346 - 0.0',
31: 'Z060 - 0.0'
};
let hsWH = {
1: 'Z971 - C1',
2: 'R943 - C2',
3: 'X702 - C3',
4: 'O128 - C4',
5: 'M555 - C5',
6: 'B041 - C6',
7: 'A641 - H',
8: 'R051 - L',
9: 'V283 - 0.0',
10: 'T458 - C12 Thera'
};
let lsWH = {
1: 'Z971 - C1',
2: 'R943 - C2',
3: 'X702 - C3',
4: 'O128 - C4',
5: 'N432 - C5',
6: 'U319 - C6',
7: 'B449 - H',
8: 'N944 - L',
9: 'S199 - 0.0',
10: 'M164 - C12 Thera'
};
let nullWH = {
1: 'Z971 - C1',
2: 'R943 - C2',
3: 'X702 - C3',
4: 'O128 - C4',
5: 'N432 - C5',
6: 'U319 - C6',
7: 'B449 - H',
8: 'N944 - L',
9: 'S199 - 0.0',
10: 'L031 - C12 Thera'
};
// ================================================================================================================
// Signature types
// ================================================================================================================
// signature types
let signatureTypes = {
return {
1: { // system type (wh)
1: { // C1 (area id)
1: { // Combat
1: 'Perimeter Ambush Point',
2: 'Perimeter Camp',
3: 'Phase Catalyst Node',
4: 'The Line'
},
2: $.extend({}, nullSecRelicSites, { // Relic
1: 'Forgotten Perimeter Coronation Platform', //*
2: 'Forgotten Perimeter Power Array' //*
}),
3: $.extend({}, nullSecDataSites, { // Data
1: 'Unsecured Perimeter Amplifier', //*
2: 'Unsecured Perimeter Information Center' //*
}),
4: { // Gas
1: 'Barren Perimeter Reservoir', //*
2: 'Token Perimeter Reservoir', //*
3: 'Minor Perimeter Reservoir', //*
4: 'Sizeable Perimeter Reservoir', //*
5: 'Ordinary Perimeter Reservoir' //*
},
5: { // Wormhole
// all k-space exits are static or K162
1: 'H121 - C1',
2: 'C125 - C2',
3: 'O883 - C3',
4: 'M609 - C4',
5: 'L614 - C5',
6: 'S804 - C6',
7: 'F353 - C12 Thera'
},
6: { // ORE
1: 'Ordinary Perimeter Deposit', //*
2: 'Common Perimeter Deposit', //*
3: 'Unexceptional Frontier Deposit', //*
4: 'Average Frontier Deposit', //*
5: 'Isolated Core Deposit', //*
6: 'Uncommon Core Deposit' //*
},
7: { // Ghost
}
1: c1Combat,
2: c1Relic,
3: c1Data,
4: c1Gas,
5: c1WH,
6: c1Ore,
7: {} // Ghost
},
2: { // C2
1: { // Combat
1: 'Perimeter Checkpoint',
2: 'Perimeter Hangar',
3: 'The Ruins of Enclave Cohort 27',
4: 'Sleeper Data Sanctuary'
},
2: $.extend({}, nullSecRelicSites, { // Relic
1: 'Forgotten Perimeter Gateway', //*
2: 'Forgotten Perimeter Habitation Coils' //*
}),
3: $.extend({}, nullSecDataSites, { // Data
1: 'Unsecured Perimeter Comms Relay', //*
2: 'Unsecured Perimeter Transponder Farm' //*
}),
4: { // Gas
1: 'Barren Perimeter Reservoir', //*
2: 'Token Perimeter Reservoir', //*
3: 'Minor Perimeter Reservoir', //*
4: 'Sizeable Perimeter Reservoir', //*
5: 'Ordinary Perimeter Reservoir' //*
},
5: { // Wormhole
// all w-space -> w-space are statics or K162
1: 'Z647 - C1',
2: 'D382 - C2',
3: 'O477 - C3',
4: 'Y683 - C4',
5: 'N062 - C5',
6: 'R474 - C6',
7: 'F135 - C12 Thera'
},
6: { // ORE
1: 'Ordinary Perimeter Deposit', //*
2: 'Common Perimeter Deposit', //*
3: 'Unexceptional Frontier Deposit', //*
4: 'Average Frontier Deposit', //*
5: 'Isolated Core Deposit', //*
6: 'Uncommon Core Deposit' //*
},
7: { // Ghost
}
1: c2Combat,
2: c2Relic,
3: c2Data,
4: c2Gas,
5: c2WH,
6: c2Ore,
7: {} // Ghost
},
3: { // C3
1: { // Combat
1: 'Fortification Frontier Stronghold',
2: 'Outpost Frontier Stronghold',
3: 'Solar Cell',
4: 'The Oruze Construct'
},
2: $.extend({}, nullSecRelicSites, { // Relic
1: 'Forgotten Frontier Quarantine Outpost', //*
2: 'Forgotten Frontier Recursive Depot' //*
}),
3: $.extend({}, nullSecDataSites, { // Data
1: 'Unsecured Frontier Database', //*
2: 'Unsecured Frontier Receiver' //*
}),
4: { // Gas
1: 'Barren Perimeter Reservoir', //*
2: 'Token Perimeter Reservoir', //*
3: 'Minor Perimeter Reservoir', //*
4: 'Sizeable Perimeter Reservoir', //*
5: 'Ordinary Perimeter Reservoir', //*
6: 'Bountiful Frontier Reservoir', //*
7: 'Vast Frontier Reservoir' //*
},
5: { // Wormhole
// all k-space exits are static or K162
1: 'V301 - C1',
2: 'I182 - C2',
3: 'N968 - C3',
4: 'T405 - C4',
5: 'N770 - C5',
6: 'A982 - C6',
7: 'F135 - C12 Thera'
},
6: { // ORE
1: 'Ordinary Perimeter Deposit', //*
2: 'Common Perimeter Deposit', //*
3: 'Unexceptional Frontier Deposit', //*
4: 'Average Frontier Deposit', //*
5: 'Infrequent Core Deposit', //*
6: 'Unusual Core Deposit' //*
},
7: { // Ghost
}
1: c3Combat,
2: c3Relic,
3: c3Data,
4: c3Gas,
5: c3WH,
6: c3Ore,
7: {} // Ghost
},
4: { // C4
1: { // Combat
1: 'Frontier Barracks',
2: 'Frontier Command Post',
3: 'Integrated Terminus',
4: 'Sleeper Information Sanctum'
},
2: { // Relic
1: 'Forgotten Frontier Conversion Module',
2: 'Forgotten Frontier Evacuation Center'
},
3: { // Data
1: 'Unsecured Frontier Digital Nexus',
2: 'Unsecured Frontier Trinary Hub'
},
4: { // Gas
1: 'Barren Perimeter Reservoir', //*
2: 'Token Perimeter Reservoir', //*
3: 'Minor Perimeter Reservoir', //*
4: 'Sizeable Perimeter Reservoir', //*
5: 'Ordinary Perimeter Reservoir', //*
6: 'Vast Frontier Reservoir', //*
7: 'Bountiful Frontier Reservoir' //*
},
5: { // Wormhole
// no *wandering* w-space -> w-space
// all holes are statics or K162
1: 'S047 - H',
2: 'N290 - L',
3: 'K329 - 0.0'
},
6: { // ORE
1: 'Ordinary Perimeter Deposit', //*
2: 'Common Perimeter Deposit', //*
3: 'Unexceptional Frontier Deposit', //*
4: 'Average Frontier Deposit', //*
5: 'Unusual Core Deposit', //*
6: 'Infrequent Core Deposit' //*
},
7: { // Ghost
}
1: c4Combat,
2: c4Relic,
3: c4Data,
4: c4Gas,
5: c4WH,
6: c4Ore,
7: {} // Ghost
},
5: { // C5
1: { // Combat
1: 'Core Garrison', //*
2: 'Core Stronghold', //*
3: 'Oruze Osobnyk', //*
4: 'Quarantine Area'
},
2: { // Relic
1: 'Forgotten Core Data Field',
2: 'Forgotten Core Information Pen'
},
3: { // Data
1: 'Unsecured Frontier Enclave Relay',
2: 'Unsecured Frontier Server Bank'
},
4: { // Gas
1: 'Barren Perimeter Reservoir', //*
2: 'Minor Perimeter Reservoir', //*
3: 'Ordinary Perimeter Reservoir', //*
4: 'Sizeable Perimeter Reservoir', //*
5: 'Token Perimeter Reservoir', //*
6: 'Bountiful Frontier Reservoir', //*
7: 'Vast Frontier Reservoir', //*
8: 'Instrumental Core Reservoir', //*
9: 'Vital Core Reservoir' //*
},
5: { // Wormhole
1: 'D792 - H',
2: 'C140 - L',
3: 'Z142 - 0.0'
},
6: { // ORE
1: 'Average Frontier Deposit', //*
2: 'Unexceptional Frontier Deposit', //*
3: 'Uncommon Core Deposit', //*
4: 'Ordinary Perimeter Deposit', //*
5: 'Common Perimeter Deposit', //*
6: 'Exceptional Core Deposit', //*
7: 'Infrequent Core Deposit', //*
8: 'Unusual Core Deposit', //*
9: 'Rarified Core Deposit', //*
10: 'Isolated Core Deposit' //*
},
7: { // Ghost
}
1: c5Combat,
2: c5Relic,
3: c5Data,
4: c5Gas,
5: c5WH,
6: c5Ore,
7: {} // Ghost
},
6: { // C6
1: { // Combat
1: 'Core Citadel', //*
2: 'Core Bastion', //*
3: 'Strange Energy Readings', //*
4: 'The Mirror' //*
},
2: { // Relic
1: 'Forgotten Core Assembly Hall', //*
2: 'Forgotten Core Circuitry Disassembler' //*
},
3: { // Data
1: 'Unsecured Core Backup Array', //*
2: 'Unsecured Core Emergence' //*
},
4: { // Gas
1: 'Barren Perimeter Reservoir', //*
2: 'Minor Perimeter Reservoir', //*
3: 'Ordinary Perimeter Reservoir', //*
4: 'Sizeable Perimeter Reservoir', //*
5: 'Token Perimeter Reservoir', //*
6: 'Bountiful Frontier Reservoir', //*
7: 'Vast Frontier Reservoir', //*
8: 'Instrumental Core Reservoir', //*
9: 'Vital Core Reservoir' //*
},
5: { // Wormhole
1: 'B520 - H',
2: 'D792 - H',
3: 'C140 - L',
4: 'C391 - L',
5: 'C248 - 0.0',
6: 'Z142 - 0.0'
},
6: { // ORE
1: 'Ordinary Perimeter Deposit', //*
2: 'Common Perimeter Deposit', //*
3: 'Unexceptional Frontier Deposit', //*
4: 'Average Frontier Deposit', //*
5: 'Rarified Core Deposit' //*
},
1: c6Combat,
2: c6Relic,
3: c6Data,
4: c6Gas,
5: c6WH,
6: c6Ore,
7: { // Ghost
1: 'Superior Blood Raider Covert Research Facility' //*
}
},
12: { // Thera wormhole
1: { // Combat
1: 'Epicenter',
2: 'Expedition Command Outpost Wreck',
3: 'Planetary Colonization Office Wreck',
4: 'Testing Facilities'
}
12: { // Thera WH
1: c12Combat
},
13: { // Shattered Wormholes
5: { // Wormhole (some of them are static)
1: 'P060 - C1',
2: 'Z647 - C1',
3: 'D382 - C2',
4: 'L005 - C2',
5: 'N766 - C2',
6: 'C247 - C3',
7: 'M267 - C3',
8: 'O477 - C3',
9: 'X877 - C4',
10: 'Y683 - C4',
11: 'H296 - C5',
12: 'H900 - C5',
13: 'H296 - C5',
14: 'N062 - C5', // ??
15: 'V911 - C5',
16: 'U574 - C6',
17: 'V753 - C6',
18: 'W237 - C6',
19: 'B274 - H',
20: 'D792 - H',
21: 'D845 - H',
22: 'N110 - H',
23: 'A239 - L',
24: 'C391 - L',
25: 'J244 - L',
26: 'U201 - L', // ??
27: 'U210 - L',
28: 'C248 - 0.0',
29: 'E545 - 0.0',
30: 'K346 - 0.0',
31: 'Z060 - 0.0'
}
13: { // Shattered WH
5: c13WH,
6: c13Ore
},
14: { // Drifter Sentinel WH
1: { // Combat
1: 'Monolith',
2: 'Wormhole in Rock Circle',
3: 'Opposing Spatial Rifts',
4: 'Sleeper Enclave Debris',
5: 'Crystal Resource'
}
1: c14Combat
},
15: { // Drifter Barbican WH
1: { // Combat
1: 'Wrecked Ships',
2: 'Unstable Wormhole',
3: 'Spatial Rift',
4: 'Heavily Guarded Spatial Rift',
5: 'Crystals'
}
1: c15Combat
},
16: { // Drifter Vidette WH
1: { // Combat
1: 'Ship Graveyard',
2: 'Sleeper Engineering Station',
3: 'Spatial Rift',
4: 'Sleeper Enclave in Coral Rock',
5: 'Crystals and Stone Circle'
}
1: c16Combat
},
17: { // Drifter Conflux WH
1: { // Combat
1: 'Monolith',
2: 'Caged Wormhole',
3: 'Rock Formation and Wormhole',
4: 'Particle Acceleration Array',
5: 'Guarded Asteroid Station'
}
1: c17Combat
},
18: { // Drifter Redoubt WH
1: { // Combat
1: 'Ship Graveyard',
2: 'Caged Wormhole',
3: 'Spatial Rift Generator',
4: 'Sleeper Enclave',
5: 'Hollow Asteroid'
}
1: c18Combat
}
}, // system type (k-space)
2: {
2: {
30: { // High Sec
5: { // Wormhole
1: 'Z971 - C1',
2: 'R943 - C2',
3: 'X702 - C3',
4: 'O128 - C4',
5: 'M555 - C5',
6: 'B041 - C6',
7: 'A641 - H',
8: 'R051 - L',
9: 'V283 - 0.0',
10: 'T458 - C12 Thera'
}
5: hsWH
},
31: { // Low Sec
5: { // Wormhole
1: 'Z971 - C1',
2: 'R943 - C2',
3: 'X702 - C3',
4: 'O128 - C4',
5: 'N432 - C5',
6: 'U319 - C6',
7: 'B449 - H',
8: 'N944 - L',
9: 'S199 - 0.0',
10: 'M164 - C12 Thera'
}
5: lsWH
},
32: { // 0.0
5: { // Wormhole
1: 'Z971 - C1',
2: 'R943 - C2',
3: 'X702 - C3',
4: 'O128 - C4',
5: 'N432 - C5',
6: 'U319 - C6',
7: 'B449 - H',
8: 'N944 - L',
9: 'S199 - 0.0',
10: 'L031 - C12 Thera'
}
5: nullWH
}
}
};
return signatureTypes;
});

View File

@@ -1,13 +1,15 @@
define([
'jquery',
'app/init',
'app/util'
], ($, Init, Util) => {
'app/util',
'app/lib/cron'
], ($, Util, Cron) => {
'use strict';
let config = {
counterDigitSmallClass: 'pf-digit-counter-small',
counterDigitLargeClass: 'pf-digit-counter-large'
counterTaskAttr: 'data-counter-task', // element attr name with initialized counter name
counterStopClass: 'stopCounter', // class for counter elements where counter should be destroyed
counterDigitSmallClass: 'pf-digit-counter-small', // class for 'small' counter DOM elements (e.g. 'hour' number)
counterDigitLargeClass: 'pf-digit-counter-large' // class for 'large' counter DOM elements (e.g. 'days' number)
};
/**
@@ -57,73 +59,58 @@ define([
}
}
element.html(parts.join(' '));
};
/**
* destroy all active counter recursive
*/
$.fn.destroyTimestampCounter = function(recursive){
return this.each(function(){
let element = $(this);
let counterSelector = '[data-counter="init"]';
let counterElements = element.filter(counterSelector);
if(recursive){
counterElements = counterElements.add(element.find(counterSelector));
}
let destroyTimestampCounter = (element, recursive) => {
let counterTaskSelector = '[' + config.counterTaskAttr + ']';
let counterElements = element.filter(counterTaskSelector);
if(recursive){
counterElements = counterElements.add(element.find(counterTaskSelector));
}
counterElements.each(function(){
let element = $(this);
let interval = element.data('interval');
if(interval){
clearInterval(interval);
element.removeAttr('data-counter').removeData('interval').removeClass('stopCounter');
}
});
counterElements.each(function(){
let element = $(this);
let taskName = element.attr(config.counterTaskAttr);
if(Cron.delete(taskName)){
element.removeAttr(config.counterTaskAttr).removeClass(config.counterStopClass);
}
});
};
/**
* init a live counter based on a unix timestamp
* @param round string e.g. 'd' => round days
* @param element
* @param round e.g. 'd' => round days
* @returns {void|*|undefined}
*/
$.fn.initTimestampCounter = function(round){
return this.each(function(){
let element = $(this);
let timestamp = parseInt( element.text() );
let initTimestampCounter = (element, round) => {
let timestamp = parseInt(element.text());
// do not init twice
if(timestamp > 0){
let taskName = element.attr('id') || Util.getRandomString();
let date = new Date( timestamp * 1000);
updateDateDiff(element, date, round);
// do not init twice
if(timestamp > 0){
// mark as init
element.attr('data-counter', 'init');
// show element (if invisible) after first update
element.css({'visibility': 'initial'});
let date = new Date( timestamp * 1000);
let counterTask = Cron.new(taskName, {precision: 'seconds', interval: 1, timeout: 100});
counterTask.task = () => {
if(element.hasClass(config.counterStopClass)){
destroyTimestampCounter(element);
}else{
updateDateDiff(element, date, round);
}
};
Cron.set(counterTask);
updateDateDiff(element, date, round);
// show element (if invisible) after first update
element.css({'visibility': 'initial'});
// calc ms until next second
// -> makes sure all counter update in sync no matter when init
let msUntilSecond = 1500 - new Date().getMilliseconds();
setTimeout(function(){
let refreshIntervalId = window.setInterval(function(){
// update element with current time
if( !element.hasClass('stopCounter')){
updateDateDiff(element, date, round);
}else{
clearInterval( element.data('interval') );
}
}, 500);
element.data('interval', refreshIntervalId);
}, msUntilSecond);
}
});
element.attr(config.counterTaskAttr, taskName);
}
};
/**
@@ -134,30 +121,33 @@ define([
*/
let initTableCounter = (tableElement, columnSelector, round) => {
let tableApi = tableElement.api();
let taskName = tableElement.attr('id');
// mark as init
tableElement.attr('data-counter', 'init');
let updateTableCount = () => {
tableApi.cells(null, columnSelector).every(function(rowIndex, colIndex, tableLoopCount, cellLoopCount){
let cell = this;
let node = cell.node();
let data = cell.data();
if(data && Number.isInteger(data) && !node.classList.contains('stopCounter')){
// timestamp expected int > 0
let date = new Date(data * 1000);
updateDateDiff( cell.nodes().to$(), date, round);
}
});
let cellUpdate = function(rowIndex, colIndex, tableLoopCount, cellLoopCount){
let cell = this;
let node = cell.node();
let data = cell.data();
if(data && Number.isInteger(data) && !node.classList.contains(config.counterStopClass)){
// timestamp expected int > 0
let date = new Date(data * 1000);
updateDateDiff(cell.nodes().to$(), date, round);
}
};
let refreshIntervalId = window.setInterval(updateTableCount, 500);
let counterTask = Cron.new(taskName, {precision: 'seconds', interval: 1, timeout: 100});
counterTask.task = timer => {
tableApi.cells(null, columnSelector).every(cellUpdate);
};
Cron.set(counterTask);
tableElement.data('interval', refreshIntervalId);
tableElement.attr(config.counterTaskAttr, taskName);
};
return {
config: config,
updateDateDiff: updateDateDiff,
initTableCounter: initTableCounter
initTimestampCounter: initTimestampCounter,
initTableCounter: initTableCounter,
destroyTimestampCounter: destroyTimestampCounter
};
});

View File

@@ -1,6 +1,7 @@
define([
'jquery',
'app/init',
'app/counter',
'app/promises/promise.deferred',
'app/promises/promise.timeout',
'datatables.net',
@@ -8,7 +9,7 @@ define([
'datatables.net-buttons-html',
'datatables.net-responsive',
'datatables.net-select'
], ($, Init, DeferredPromise, TimeoutPromise) => {
], ($, Init, Counter, DeferredPromise, TimeoutPromise) => {
'use strict';
// all Datatables stuff is available...
@@ -42,7 +43,7 @@ define([
}
// remove all active counters in table
table.destroyTimestampCounter(true);
Counter.destroyTimestampCounter(table, true);
});
// Status Plugin ==============================================================================================

View File

@@ -3,10 +3,20 @@
*/
define([], () => {
'use strict';
let Config = {
let frigWH = {
1: 'E004 - C1',
2: 'L005 - C2',
3: 'Z006 - C3',
4: 'M001 - C4',
5: 'C008 - C5',
6: 'G008 - C6',
7: 'Q003 - 0.0',
8: 'A009 - C13'
};
return {
path: {
img: '/public/img/', // path for images
api: '/api/rest', //ajax URL - REST API
@@ -49,12 +59,12 @@ define([], () => {
gitHubReleases: '/api/github/releases' // ajax URL - get release info from GitHub
},
breakpoints: [
{ name: 'screen-xl', width: Infinity },
{ name: 'screen-l', width: 1600 },
{ name: 'screen-m', width: 1200 },
{ name: 'screen-d', width: 1000 },
{ name: 'screen-s', width: 780 },
{ name: 'screen-xs', width: 480 }
{name: 'screen-xl', width: Infinity},
{name: 'screen-l', width: 1600},
{name: 'screen-m', width: 1200},
{name: 'screen-d', width: 1000},
{name: 'screen-s', width: 780},
{name: 'screen-xs', width: 480}
],
animationSpeed: {
splashOverlay: 300, // "splash" loading overlay
@@ -95,63 +105,63 @@ define([], () => {
class: 'fa-desktop',
label: 'desktop',
unicode: '&#xf108;'
},{
}, {
class: 'fa-space-shuttle',
label: 'space shuttle',
unicode: '&#xf197;'
},{
}, {
class: 'fa-anchor',
label: 'anchor',
unicode: '&#xf13d;'
},{
}, {
class: 'fa-satellite',
label: 'satellite',
unicode: '&#xf7bf;'
},{
}, {
class: 'fa-skull-crossbones',
label: 'skull crossbones',
unicode: '&#xf714;'
},{
}, {
class: 'fa-fire',
label: 'fire',
unicode: '&#xf06d;'
},{
}, {
class: 'fa-bookmark',
label: 'bookmark',
unicode: '&#xf02e;'
},{
}, {
class: 'fa-cube',
label: 'cube',
unicode: '&#xf1b2;'
},{
}, {
class: 'fa-star',
label: 'star',
unicode: '&#xf005;'
},{
}, {
class: 'fa-hat-wizard',
label: 'hat wizard',
unicode: '&#xf6e8;'
},{
}, {
class: 'fa-plane',
label: 'plane',
unicode: '&#xf072;'
},{
}, {
class: 'fa-globe',
label: 'globe',
unicode: '&#xf0ac;'
},{
}, {
class: 'fa-rocket',
label: 'rocket',
unicode: '&#xf135;'
},{
}, {
class: 'fa-life-ring',
label: 'life ring',
unicode: '&#xf1cd;'
},{
}, {
class: 'fa-heart',
label: 'heart',
unicode: '&#xf004;'
},{
}, {
class: 'fa-poop',
label: 'poop',
unicode: '&#xf619;'
@@ -541,106 +551,16 @@ define([], () => {
},
// frigate wormholes
frigateWormholes: {
1: { // C1
1: 'E004 - C1',
2: 'L005 - C2',
3: 'Z006 - C3',
4: 'M001 - C4',
5: 'C008 - C5',
6: 'G008 - C6',
7: 'Q003 - 0.0',
8: 'A009 - C13'
},
2: { // C2
1: 'E004 - C1',
2: 'L005 - C2',
3: 'Z006 - C3',
4: 'M001 - C4',
5: 'C008 - C5',
6: 'G008 - C6',
7: 'Q003 - 0.0',
8: 'A009 - C13'
},
3: { // C3
1: 'E004 - C1',
2: 'L005 - C2',
3: 'Z006 - C3',
4: 'M001 - C4',
5: 'C008 - C5',
6: 'G008 - C6',
7: 'Q003 - 0.0',
8: 'A009 - C13'
},
4: { // C4
1: 'E004 - C1',
2: 'L005 - C2',
3: 'Z006 - C3',
4: 'M001 - C4',
5: 'C008 - C5',
6: 'G008 - C6',
7: 'Q003 - 0.0',
8: 'A009 - C13'
},
5: { // C5
1: 'E004 - C1',
2: 'L005 - C2',
3: 'Z006 - C3',
4: 'M001 - C4',
5: 'C008 - C5',
6: 'G008 - C6',
7: 'Q003 - 0.0',
8: 'A009 - C13'
},
6: { // C6
1: 'E004 - C1',
2: 'L005 - C2',
3: 'Z006 - C3',
4: 'M001 - C4',
5: 'C008 - C5',
6: 'G008 - C6',
7: 'Q003 - 0.0',
8: 'A009 - C13'
},
13: { // Shattered Wormholes (some of them are static)
1: 'E004 - C1',
2: 'L005 - C2',
3: 'Z006 - C3',
4: 'M001 - C4',
5: 'C008 - C5',
6: 'G008 - C6',
7: 'Q003 - 0.0',
8: 'A009 - C13'
},
30: { // High Sec
1: 'E004 - C1',
2: 'L005 - C2',
3: 'Z006 - C3',
4: 'M001 - C4',
5: 'C008 - C5',
6: 'G008 - C6',
7: 'Q003 - 0.0',
8: 'A009 - C13'
},
31: { // Low Sec
1: 'E004 - C1',
2: 'L005 - C2',
3: 'Z006 - C3',
4: 'M001 - C4',
5: 'C008 - C5',
6: 'G008 - C6',
7: 'Q003 - 0.0',
8: 'A009 - C13'
},
32: { // 0.0
1: 'E004 - C1',
2: 'L005 - C2',
3: 'Z006 - C3',
4: 'M001 - C4',
5: 'C008 - C5',
6: 'G008 - C6',
7: 'Q003 - 0.0',
8: 'A009 - C13'
}
1: frigWH, // C1
2: frigWH, // C2
3: frigWH, // C3
4: frigWH, // C4
5: frigWH, // C5
6: frigWH, // C6
13: frigWH, // Shattered Wormholes (some of them are static)
30: frigWH, // High Sec
31: frigWH, // Low Sec
32: frigWH // 0.0
},
// Drifter wormholes (can only appear in k-space)
drifterWormholes: {
@@ -660,8 +580,5 @@ define([], () => {
6: 'K162 - 0.0',
7: 'K162 - C12 Thera'
}
};
return Config;
});

275
js/app/lib/cache.js Normal file
View File

@@ -0,0 +1,275 @@
define([], () => {
'use strict';
/**
* Abstract Cache Strategy class
* @type {AbstractStrategy}
*/
let AbstractStrategy = class AbstractStrategy {
constructor(){
if(new.target === AbstractStrategy){
throw new TypeError('Cannot construct AbstractStrategy instances directly');
}
}
/**
* factory for Strategy* instances
* @returns {AbstractStrategy}
*/
static create(){
return new this();
}
};
/**
* LIFO Cache Strategy - First In First Out
* -> The cache evicts the entries in the order they were added,
* without any regard to how often or how many times they were accessed before.
* @type {StrategyFIFO}
*/
let StrategyFIFO = class StrategyFIFO extends AbstractStrategy {
valueToCompare(metaData){
return metaData.age();
}
compare(a, b){
return b - a;
}
};
/**
* LFU Cache Strategy - Least Frequently Used
* -> The cache evicts the entries in order how often have been accessed.
* Those that are used least often are discarded first
* @type {StrategyLFU}
*/
let StrategyLFU = class StrategyLFU extends AbstractStrategy {
valueToCompare(metaData){
return metaData.hitCount;
}
compare(a, b){
return a - b;
}
};
/**
* LRU Cache Strategy - Least Recently Used
* -> The cache evicts entries that have not been used for the longest amount of time.
* No matter how often they have been accessed.
* @type {StrategyLRU}
*/
let StrategyLRU = class StrategyLRU extends AbstractStrategy {
valueToCompare(metaData){
return metaData.hits[metaData.hits.length - 1] || metaData.set;
}
compare(a, b){
return a - b;
}
};
/**
* Each entry in cache also has its own instance of CacheEntryMeta
* -> The configured Cache Strategy use this meta data for eviction policy
* @type {CacheEntryMeta}
*/
let CacheEntryMeta = class CacheEntryMeta {
constructor(ttl, tSet){
this.ttl = ttl;
this.tSet = tSet || this.constructor.now();
this.tHits = [];
}
get set(){
return this.tSet;
}
get hits(){
return this.tHits;
}
get hitCount(){
return this.hits.length;
}
newHit(current){
this.tHits.push(current || this.constructor.now());
}
age(current){
return (current || this.constructor.now()) - this.tSet;
}
expired(current){
return this.ttl < this.age(current);
}
static now(){
return new Date().getTime() / 1000;
}
static create(ttl, tSet){
return new this(ttl, tSet);
}
};
/**
* Each instance of Cache represents a key value in memory data store
* -> Name should be set to identify current Cache instance
* -> Default ttl can be overwritten by individual entries
* -> Cache Strategy handles eviction policy
* -> Buffer Size (in percent) can be used to remove e.g. 10% of all entries
* if cache reaches maxSize limit, to increase performance.
* @type {Cache}
*/
let Cache = class Cache {
constructor(config){
this.config = Object.assign({},{
name: 'Default', // custom name for identification
ttl: 3600, // default ttl for cache entries
maxSize: 600, // max cache entries
bufferSize: 10, // cache entry count in percent to be removed if maxSize reached
strategy: 'FIFO', // cache strategy policy
debug: false // debug output in console
}, config);
this.store = new Map();
this.metaStore = new WeakMap();
this.strategy = this.constructor.setStrategy(this.config.strategy);
this.debug = (msg,...data) => {
if(this.config.debug){
data = (data || []);
data.unshift(this.config.name);
console.debug('debug: CACHE %o | ' + msg, ...data);
}
};
this.debug('New Cache instance');
}
get size(){
return this.store.size;
}
isFull(){
return this.size>= this.config.maxSize;
}
set(key, value, ttl){
if(this.store.has(key)){
this.debug('SET key %o, UPDATE value %o', key, value);
this.store.set(key, value);
}else{
this.debug('SET key %o, NEW value %o', key, value);
if(this.isFull()){
this.debug(' ↪ FULL trim cache…');
this.trim(this.trimCount(1));
}
this.store.set(key, value);
}
this.metaStore.set(value, CacheEntryMeta.create(ttl || this.config.ttl));
}
get(key){
if(this.store.has(key)){
let value = this.store.get(key);
if(value){
let metaData = this.metaStore.get(value);
if(metaData.expired()){
this.debug('EXPIRED key %o delete', key);
this.delete(key);
}else{
this.debug('HIT key %o', key);
metaData.newHit();
return value;
}
}
}
this.debug('MISS key %o', key);
}
getOrDefault(key, def){
return this.get(key) || def;
}
keysForTrim(count){
let trimKeys = [];
let compare = [];
for(let [key, value] of this.store){
let metaData = this.metaStore.get(value);
if(metaData.expired()){
trimKeys.push(key);
if(count === trimKeys.length){
break;
}
}else{
compare.push({
key: key,
value: this.strategy.valueToCompare(metaData)
});
}
}
let countLeft = count - trimKeys.length;
if(countLeft > 0){
compare = compare.sort((a, b) => this.strategy.compare(a.value, b.value));
trimKeys = trimKeys.concat(compare.splice(0, countLeft).map(a => a.key));
}
return trimKeys;
}
keys(){
return this.store.keys();
}
delete(key){
return this.store.delete(key);
}
clear(){
this.store.clear();
}
trimCount(spaceLeft){
let bufferSize = Math.max(Math.round(this.config.maxSize / 100 * this.config.bufferSize), spaceLeft);
return Math.min(Math.max(this.size - this.config.maxSize + bufferSize, 0), this.size);
}
trim(count){
if(count > 0){
let trimKeys = this.keysForTrim(count);
if(count > trimKeys.length){
console.warn(' ↪ Failed to trim(%i) entries. Only %i in store', count, this.size);
}
this.debug(' ↪ DELETE min %i keys: %o', count, trimKeys);
trimKeys.forEach(key => this.delete(key));
}
}
status(){
return {
config: this.config,
store: this.store,
metaStore: this.metaStore
};
}
static setStrategy(name){
switch(name){
case 'FIFO': return StrategyFIFO.create();
case 'LFU': return StrategyLFU.create();
case 'LRU': return StrategyLRU.create();
default:
throw new ReferenceError('Unknown cache strategy name: ' + name);
}
}
};
return Cache;
});

View File

@@ -29,6 +29,9 @@ define([], () => {
'line-height': '19px',
'font-family': '"Fira Code", "Lucida Console"',
},
'debug': {
'color': '#d747d6'
},
'ok': {
'color': '#5cb85c'
},
@@ -113,8 +116,8 @@ define([], () => {
let setLineStyleByLogType = (logType, args) => {
if(args.length){
let lineStyle = getStyleByLogType('global') + getStyleByLogType(logType);
lineStyle += ['ok', 'log', 'info', 'pf'].includes(logType) ? getStyleByLogType('indentDefault') : '';
let bullet = ['ok', 'log', 'info', 'pf'].includes(logType) ? '●' : '';
lineStyle += ['debug', 'ok', 'log', 'info', 'pf'].includes(logType) ? getStyleByLogType('indentDefault') : '';
let bullet = ['debug', 'ok', 'log', 'info', 'pf'].includes(logType) ? '●' : '';
if(typeof args[0] === 'string'){
// prepend placeholder to existing message
@@ -150,6 +153,12 @@ define([], () => {
}
};
origConsole.debug = (...args) => {
setMessageStyleByLogType('debug', args);
setLineStyleByLogType('debug', args);
info.apply(origConsole, args);
};
origConsole.ok = (...args) => {
setMessageStyleByLogType('ok', args);
setLineStyleByLogType('ok', args);

322
js/app/lib/cron.js Normal file
View File

@@ -0,0 +1,322 @@
define([
'easyTimer',
'app/promises/promise.timeout',
], (easytimer, TimeoutPromise) => {
'use strict';
/*
Example1 run task every second ------------------------------------------------------------------------------------
let task1 = Cron.new('task1', {precision: 'seconds', interval: 1, timeout: 100});
task1.task = (timer) => {
console.info('task1 function():', timer.getTotalTimeValues());
return 'OK';
};
Cron.set(task1);
Example2 run task every 3 seconds ---------------------------------------------------------------------------------
let task1 = Cron.new('task1', {precision: 'seconds', interval: 3, timeout: 100});
task1.task = (timer) => {
console.info('task1 function():', timer.getTotalTimeValues());
return 'OK';
};
Cron.set(task1);
Example3 resolve Promise on run ----------------------------------------------------------------------------------
let task1 = Cron.new('task1', {precision: 'seconds', interval: 1, timeout: 100});
task1.task = (timer, task) => {
return new Promise((resolve, reject) => {
console.info('task1 Promise1():', timer.getTotalTimeValues(), task.get('interval'));
//task.set('interval', task.get('interval') + 1) // increase run interval every time by 1s
resolve('OK1');
}).then(payload => {
return new Promise((resolve, reject) => {
console.info('task2 Promise2():', timer.getTotalTimeValues(), payload);
resolve('OK2');
});
});
};
Cron.set(task1);
Example4 run task once at given Date() --------------------------------------------------------------------------
let dueDate = new Date();
dueDate.setSeconds(dueDate.getSeconds() + 5);
let task2 = Cron.new('task2', {precision: 'seconds', timeout: 100, dueDate: dueDate});
task2.task = () => 'OK task2';
Cron.set(task2);
*/
/**
* Task instances represent a task that should be executed at a given interval or dueDate
* -> Task´s are managed by CronManager()
* @type {Task}
*/
let Task = class Task {
constructor(name, config){
if(typeof name !== 'string'){
throw new TypeError('Task "name" must be instance of String, Type of "' + typeof name + '" given');
}
this._config = Object.assign({}, this.constructor.defaultConfig, config);
this._name = name; // unique name for identification
this._task = undefined; // task to run, instanceof Function, can also return a Promise
this._manager = undefined; // reference to CronManager() that handles this task
this._runQueue = new Map(); // current run() processes. > 1 requires config.isParallel: true
this._runCount = 0; // total run counter for this task
this._lastTotalTimeValues = undefined; // time values of last run()
}
get name(){
return this._name;
}
get task(){
return this._task;
}
get runCount(){
return this._runCount;
}
get precision(){
return this._config.precision;
}
set task(task){
if(task instanceof Function){
this._task = task;
}else{
throw new TypeError('Task "task" must be instance of "function", Type of "' + typeof task + '" given');
}
}
get(option){
return this._config[option];
}
set(option, value){
this._config[option] = value;
}
setManager(manager){
this._manager = manager;
}
isRunning(){
return !!this._runQueue.size;
}
delete(){
let isDeleted = false;
if(this._manager){
isDeleted = this._manager.delete(this.name);
}
return isDeleted;
}
isDue(timer){
if(this._config.dueDate instanceof Date){
// run once at dueDate
if(new Date().getTime() >= this._config.dueDate.getTime()){
return true;
}
}else{
// periodic execution
let totalTimeValues = timer.getTotalTimeValues();
let totalTimeValuePrecision = totalTimeValues[this.precision];
totalTimeValuePrecision -= this._lastTotalTimeValues ? this._lastTotalTimeValues[this.precision] : 0;
if(
this._config.interval === 1 ||
totalTimeValuePrecision % this._config.interval === 0
){
return true;
}
}
return false;
}
invoke(timer){
if(
this.isDue(timer) &&
(!this.isRunning() || this._config.isParallel)
){
this.run(timer);
}
}
run(timer){
this._lastTotalTimeValues = Object.assign({}, timer.getTotalTimeValues());
let runId = 'run_' + (++this._runCount);
let runExec = resolve => {
resolve(this.task(timer, this));
};
let myProm = this._config.timeout > 0 ? new TimeoutPromise(runExec, this._config.timeout) : new Promise(runExec);
myProm.then(payload => {
// resolved within timeout -> wait for finally() block
}).catch(error => {
if(error instanceof Error){
// either timeout error or error from rejected deferredPromise
console.warn(error);
}
}).finally(() => {
// no matter if TimeoutPromise is resolved or rejected
// -> remove from _runQueue
this._runQueue.delete(runId);
// remove this task from store after run
if(this._config.dueDate instanceof Date){
this.delete();
}
});
this._runQueue.set(runId, myProm);
}
};
Task.defaultConfig = {
precision: 'seconds', // updateEvent this tasked will be subscribed to
isParallel: false, // if true this task can run parallel, e.g. if prev execution has not finished
interval: 1, // relates to 'precision'. 'interval' = 3 and 'precision' = "seconds" -> run every 3 seconds
dueDate: undefined, // if Date() instance is set, task only runs once at dueDate
timeout: 50 // if > 0, execution time that exceeds timeout (ms) throw error
};
/**
* An instance of CronManager() handles multiple Task()´s
* -> Task()´s can be set()/delete() from CronManager() instance
* @type {CronManager}
*/
let CronManager = class CronManager {
constructor(config){
this._config = Object.assign({}, this.constructor.defaultConfig, config);
this._timerConfig = Object.assign({}, this.constructor.defaultTimerConfig);
this._tasks = new Map();
this._timer = new easytimer.Timer();
// init Easytimer update events
this._config.precisions.map(precision => precision + 'Updated').forEach(eventName => {
this._timer.on(eventName, e => {
let precision = e.type.substring(0, e.type.indexOf('Updated'));
this.tasksByPrecision(precision).forEach(task => task.invoke(e.detail.timer));
});
});
this.debug = (msg,...data) => {
if(this._config.debug){
data = (data || []);
console.debug(msg, ...data);
}
};
}
new(name, config){
return new Task(name, config);
}
set(task){
if(task instanceof Task){
// check for unique task name, or update existing task
if(!this.has(task.name) || (this.get(task.name) === task)){
// set new or update existing task
task.setManager(this);
this._tasks.set(task.name, task);
this.debug('SET/UPDATE task: %o config: %o', task.name, task);
// start timer (if it is not already running)
this.auto();
}else{
console.warn('FAILED to set task. Task name %o already exists', task.name);
}
}else{
throw new TypeError('Parameter must be instance of Task');
}
}
setNew(name, config){
this.set(this.new(name, config));
}
get(name){
return this._tasks.get(name);
}
has(name){
return this._tasks.has(name);
}
delete(name){
let isDeleted = this._tasks.delete(name);
if(isDeleted){
this.debug('DELETE task: %o', name);
this.auto();
}
return isDeleted;
}
clear(){
this.debug('CLEAR all %o task(s)', this._tasks.size);
this._tasks.clear();
this.auto();
}
tasksByPrecision(precision){
let tasks = [];
this._tasks.forEach(task => {
if(precision === task.precision){
tasks.push(task);
}
});
return tasks;
}
// EasyTimer controls -----------------------------------------------------------------------------------------
start(){
this._timer.start(this._timerConfig);
}
stop(){
this._timer.stop();
}
pause(){
this._timer.pause();
}
reset(){
this._timer.reset();
}
auto(){
if(this._tasks.size){
if(!this._timer.isRunning()){
this.start();
this.debug('START [auto] timer. %o task(s) found.', this._tasks.size);
}
}else{
this.stop();
this.debug('STOP [auto] timer. No tasks set.');
}
}
};
CronManager.defaultConfig = {
precisions: [
'secondTenths',
'seconds',
'minutes',
'hours',
'days'
],
debug: false // debug output in console
};
CronManager.defaultTimerConfig = {
precision: 'secondTenths', // Timer update frequency. Values: 'secondTenths', 'seconds', 'minutes', 'hours'
countdown: false // If true, the timer is a countdown
};
return new CronManager({
debug: false
});
});

406
js/app/lib/dragSelect.js Normal file
View File

@@ -0,0 +1,406 @@
define(['app/lib/eventHandler'], (EventHandler) => {
'use strict';
let DragSelect = class DragSelect {
constructor(config){
this._config = Object.assign({}, this.constructor.defaultConfig, config);
this._instanceId = ++this.constructor.instanceCount;
this._instanceName = [this._config.namespace, this._instanceId].join('-');
this._animationFrameId = null;
this._mouseIsDown = false;
this._cursorPosition = {x: 0, y: 0};
this._selectBoxDimHash = undefined;
this._deselectedElements = [];
this._targetDim = {
left: 0,
top: 0,
width: 10,
height: 10
};
this._selectBoxOrigin = {
left: 0,
top: 0
};
this.init();
this.debug = (msg,...data) => {
if(this._config.debug){
data = (data || []);
console.debug(msg, ...data);
}
};
this.debugEvent = e => {
if(this._config.debugEvents){
let arrow = '?';
switch(e.type){
case 'mousedown': arrow = '⯆'; break;
case 'mousemove': arrow = '⯈'; break;
case 'mouseup': arrow = '⯅'; break;
}
this.debug('ON ' + arrow + ' %s currentTarget: %o event: %o', e.type, e.currentTarget, e);
}
};
}
/**
* set/update target target element dimension
* -> must be updated if target element is scrolled/or pushed by e.g. slide menu
*/
setTargetDimensions(){
let domRect = this._config.target.getBoundingClientRect();
Object.assign(this._targetDim, this.filterDomRect(domRect));
}
/**
* set/update boundary element dimension [optional]
* -> required for 'intersection' check e.g. for scrollable target
* @returns {DOMRect}
*/
setBoundaryDimensions(){
if(this._config.boundary){
let boundary = this._config.target.closest(this._config.boundary);
if(boundary){
return (this._boundaryDim = boundary.getBoundingClientRect());
}
}
delete this._boundaryDim;
}
/**
* set/update current cursor coordinates
* @param e
*/
setCursorPosition(e){
Object.assign(this._cursorPosition, {
x: e.pageX,
y: e.pageY
});
}
init(){
this.initSelectBox();
this.setTargetDimensions();
EventHandler.addEventListener(this._config.target, this.getNamespaceEvent('mousedown'), this.onMouseDown.bind(this), {passive: false});
EventHandler.addEventListener(this._config.container, this.getNamespaceEvent('mousemove'), this.onMouseMove.bind(this), {passive: false});
EventHandler.addEventListener(this._config.container, this.getNamespaceEvent('mouseup'), this.onMouseUp.bind(this), {passive: true});
}
// Mouse events -----------------------------------------------------------------------------------------------
onMouseDown(e){
if(e.which === 1){
this.debugEvent(e);
this.setTargetDimensions();
this.showSelectBox(e);
this._mouseIsDown = true;
}
}
onMouseMove(e){
if(this._mouseIsDown){
e.preventDefault();
if(this._animationFrameId){
cancelAnimationFrame(this._animationFrameId);
}
this._animationFrameId = requestAnimationFrame(() => {
this.debugEvent(e);
this.setCursorPosition(e);
this.update();
this._animationFrameId = null;
});
}
}
onMouseUp(e){
this.debugEvent(e);
this.selectElements();
this.hideSelectBox();
this._mouseIsDown = false;
this._deselectedElements = [];
}
// SelectBox handler ------------------------------------------------------------------------------------------
/**
* create new selectBox and append to DOM
* -> hidden by CSS until selectBox gets updated
*/
initSelectBox(){
this._selectBox = document.createElement('div');
this._selectBox.id = this._instanceName;
this._selectBox.classList.add(this._config.selectBoxClass);
this._selectBox.style.position = 'absolute';
this._config.target.after(this._selectBox);
}
/**
* show selectBox -> apply CSS for positioning and dimension
* @param e
*/
showSelectBox(e){
Object.assign(this._selectBoxOrigin, {
left: e.pageX - this._targetDim.left,
top: e.pageY - this._targetDim.top
});
Object.assign(this._selectBox.style,{
left: this._selectBoxOrigin.left + 'px',
top: this._selectBoxOrigin.top + 'px',
width: '1px',
height: '1px'
});
this._selectBox.classList.add(this._config.activeClass);
this.callback('onShow');
}
/**
* update selectBox position and dimension based on cursorPosition
* @returns {boolean}
*/
updateSelectBox(){
let updated = false;
if(!this.isActiveSelectBox()){
return updated;
}
let left = this._cursorPosition.x - this._targetDim.left;
let top = this._cursorPosition.y - this._targetDim.top;
let tempWidth = this._selectBoxOrigin.left - left;
let tempHeight = this._selectBoxOrigin.top - top;
let newLeft = this._selectBoxOrigin.left;
let newTop = this._selectBoxOrigin.top;
let newWidth = left - this._selectBoxOrigin.left;
let newHeight = top - this._selectBoxOrigin.top;
if(newWidth < 0){
newLeft = newLeft - tempWidth;
newWidth = newWidth * -1;
}
if(newHeight < 0){
newTop = newTop - tempHeight;
newHeight = newHeight * -1;
}
// check if dimension has changed -> improve performance
let dimensionHash = [newWidth, newHeight].join('_');
if(this._selectBoxDimHash !== dimensionHash){
this._selectBoxDimHash = dimensionHash;
Object.assign(this._selectBox.style,{
left: newLeft + 'px',
top: newTop + 'px',
width: newWidth + 'px',
height: newHeight + 'px'
});
// set drag position data (which corner)
this._selectBox.dataset.origin = this.getSelectBoxDragOrigin(left, top, newLeft, newTop).join('|');
updated = true;
this.callback('onUpdate');
this.dispatch(this._selectBox, 'update', this);
}
return updated;
}
/**
* hide selectBox
*/
hideSelectBox(){
if(!this.isActiveSelectBox()){
return;
}
if(this.callback('onHide', this._deselectedElements) !== false){
this._selectBox.classList.remove(this._config.activeClass);
}
}
/**
* cursor position corner for selectBox
* @param left
* @param top
* @param newLeft
* @param newTop
* @returns {[string, string]}
*/
getSelectBoxDragOrigin(left, top, newLeft, newTop){
let position = [];
if(
left === newLeft &&
top === newTop
){
position = ['top', 'left'];
}else if(top === newTop){
position = ['top', 'right'];
}else if(left === newLeft){
position = ['bottom', 'left'];
}else{
position = ['bottom', 'right'];
}
return position;
}
/**
* check if there is currently an active selectBox visible
* @returns {boolean}
*/
isActiveSelectBox(){
return this._selectBox.classList.contains(this._config.activeClass) &&
!this._config.target.classList.contains(this._config.disabledClass);
}
// Element select methods -------------------------------------------------------------------------------------
selectableElements(){
return this._config.target.querySelectorAll(this._config.selectables);
}
selectElements(){
if(!this.isActiveSelectBox()){
return;
}
let selectables = this.selectableElements();
let selectBoxDim = this.filterDomRect(this._selectBox.getBoundingClientRect());
selectables.forEach(el => {
let elDim = this.filterDomRect(el.getBoundingClientRect());
if(this.percentCovered(selectBoxDim, elDim) > this._config.percentCovered){
el.classList.add(this._config.selectedClass);
// remove element from "deselected" elements (e.g on add -> remove -> add scenario)
this._deselectedElements = this._deselectedElements.filter(tempEl => tempEl !== el);
}else{
if(el.classList.contains(this._config.selectedClass)){
el.classList.remove(this._config.selectedClass);
// add to "deselected" elements, if not already in array
if(this._deselectedElements.findIndex(tempEl => tempEl === el) === -1){
this._deselectedElements.push(el);
}
}
}
});
}
percentCovered(dim1, dim2){
if(
(dim1.left <= dim2.left) &&
(dim1.top <= dim2.top) &&
((dim1.left + dim1.width) >= (dim2.left + dim2.width)) &&
((dim1.top + dim1.height) > (dim2.top + dim2.height))
){
// The whole thing is covering the whole other thing
return 100;
}else{
// Only parts may be covered, calculate percentage
dim1.right = dim1.left + dim1.width;
dim1.bottom = dim1.top + dim1.height;
dim2.right = dim2.left + dim2.width;
dim2.bottom = dim2.top + dim2.height;
let l = Math.max(dim1.left, dim2.left);
let r = Math.min(dim1.right, dim2.right);
let t = Math.max(dim1.top, dim2.top);
let b = Math.min(dim1.bottom, dim2.bottom);
if(b >= t && r >= l){
return (((r - l) * (b - t)) / (dim2.width * dim2.height)) * 100;
}
}
return 0;
}
// Boundary intersection methods ------------------------------------------------------------------------------
getIntersection(){
let intersection = [];
if(this.isActiveSelectBox && this._boundaryDim){
let selectBoxDim = this._selectBox.getBoundingClientRect();
if(this._boundaryDim.top > selectBoxDim.top){
intersection.push('top');
}
if(this._boundaryDim.bottom < selectBoxDim.bottom){
intersection.push('bottom');
}
if(this._boundaryDim.left > selectBoxDim.left){
intersection.push('left');
}
if(this._boundaryDim.right < selectBoxDim.right){
intersection.push('right');
}
}
return intersection;
}
// Util methods -----------------------------------------------------------------------------------------------
update(){
this.setTargetDimensions();
this.setBoundaryDimensions();
if(this.updateSelectBox() && this._config.selectOnDrag){
this.selectElements();
}
}
getNamespaceEvent(type){
return [type, this._instanceName].join('.');
}
filterDomRect(domRect, filteredKeys = ['left', 'top', 'width', 'height']){
let obj = {};
filteredKeys.forEach(key => {
if(domRect[key] !== undefined){
obj[key] = domRect[key];
}
});
return obj;
//return filteredKeys.reduce((obj, key) => ({ ...obj, [key]: domRect[key] }), {}); // same result but uses "object destruction" ES5
}
callback(callback, ...args){
if(this._config[callback] instanceof Function){
return this._config[callback](...args);
}
}
dispatch(target, type, data = null){
let event = new CustomEvent([type, this._config.namespace].join(':'), {
bubbles: true,
detail: data
});
target.dispatchEvent(event);
}
};
DragSelect.defaultConfig = {
container: document,
target: document.body,
namespace: 'dragSelect',
activeClass: 'active',
disabledClass: 'disabled',
selectables: 'div',
selectedClass: 'dragSelect-selected',
selectBoxClass: 'dragSelect-selectBox',
boundary: undefined, // optional selector for boundary parent box (e.g. scrollable viewport)
selectOnDrag: true,
percentCovered: 25,
onShow: undefined,
onHide: undefined,
onUpdate: undefined,
debug: false,
debugEvents: false
};
DragSelect.instanceCount = 0;
return DragSelect;
});

View File

@@ -0,0 +1,29 @@
define([], () => {
'use strict';
let EventHandler = class EventHandler {
constructor(){
this._listeners = new Map();
}
addEventListener(target, type, listener, options){
this._listeners.set(type, listener);
target.addEventListener(this.constructor.eventParts(type).event, listener, options);
}
removeEventListener(target, type){
target.removeEventListener(this.constructor.eventParts(type).event, this._listeners.get(type));
this._listeners.delete(type);
}
static eventParts(type){
return type.split('.').reduce((acc, val, i) => {
acc[i ? 'namespace' : 'event'] = val;
return acc;
}, {});
}
};
return new EventHandler();
});

95
js/app/lib/prototypes.js vendored Normal file
View File

@@ -0,0 +1,95 @@
define([], () => {
'use strict';
/**
* Array diff
* [1,2,3,4,5].diff([4,5,6]) => [1,2,3]
* @param a
* @returns {*[]}
*/
Array.prototype.diff = function(a){
return this.filter(i => !a.includes(i));
};
/**
* Array intersect
* [1,2,3,4,5].intersect([4,5,6]) => [4,5]
* @param a
* @returns {*[]}
*/
Array.prototype.intersect = function(a){
return this.filter(i => a.includes(i));
};
/**
* compares two arrays if all elements in a are also in b
* element order is ignored
* @param a
* @returns {boolean}
*/
Array.prototype.equalValues = function(a){
return this.diff(a).concat(a.diff(this)).length === 0;
};
/**
* like Array.concat() + remove duplicate values
* @see https://stackoverflow.com/a/38940354/4329969
* @param a
* @returns {*[]}
*/
Array.prototype.concatFilter = function(a){
return [...new Set([...this,...a])];
};
/**
* sort array of objects by property name
* @param p
* @returns {Array.<T>}
*/
Array.prototype.sortBy = function(p){
return this.slice(0).sort((a,b) => {
return (a[p] > b[p]) ? 1 : (a[p] < b[p]) ? -1 : 0;
});
};
/**
* capitalize first letter
* @returns {string}
*/
String.prototype.capitalize = function(){
return this.charAt(0).toUpperCase() + this.slice(1);
};
/**
* get hash from string
* @returns {number}
*/
String.prototype.hashCode = function(){
let hash = 0, i, chr;
if(this.length === 0) return hash;
for(i = 0; i < this.length; i++){
chr = this.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
};
String.prototype.trimLeftChars = function(charList){
if(charList === undefined)
charList = '\\s';
return this.replace(new RegExp('^[' + charList + ']+'), '');
};
String.prototype.trimRightChars = function(charList){
if(charList === undefined)
charList = '\\s';
return this.replace(new RegExp('[' + charList + ']+$'), '');
};
String.prototype.trimChars = function(charList){
return this.trimLeftChars(charList).trimRightChars(charList);
};
return {};
});

View File

@@ -6,8 +6,9 @@ define([
'jquery',
'app/init',
'app/util',
'app/counter',
'bootbox'
], ($, Init, Util, bootbox) => {
], ($, Init, Util, Counter, bootbox) => {
'use strict';
@@ -31,11 +32,11 @@ define([
* updated "sync status" dynamic dialog area
*/
let updateSyncStatus = () => {
// check if task manager dialog is open
let logDialog = $('#' + config.taskDialogId);
if(logDialog.length){
// dialog is open
let statusArea = logDialog.find('.' + config.taskDialogStatusAreaClass);
requirejs(['text!templates/modules/sync_status.html', 'mustache'], (templateSyncStatus, Mustache) => {
let data = {
timestampCounterClass: config.timestampCounterClass,
@@ -49,10 +50,12 @@ define([
};
let syncStatusElement = $(Mustache.render(templateSyncStatus, data ));
Counter.destroyTimestampCounter(statusArea, true);
logDialog.find('.' + config.taskDialogStatusAreaClass).html( syncStatusElement );
statusArea.html(syncStatusElement);
logDialog.find('.' + config.timestampCounterClass).initTimestampCounter();
let counterElements = syncStatusElement.find('.' + config.timestampCounterClass);
Counter.initTimestampCounter(counterElements);
syncStatusElement.initTooltips({
placement: 'right'

View File

@@ -689,6 +689,7 @@ define([
link: this.characterElement.data('href'),
cookieName: this.cookieName,
browserTabId: this.browserTabId,
ccpImageServer: responseData.ccpImageServer,
character: responseData.character,
isManager: Util.getObjVal(responseData, 'character.role.name') === 'CORPORATION',
isAdmin: Util.getObjVal(responseData, 'character.role.name') === 'SUPER',

View File

@@ -15,6 +15,9 @@ define([
endpointContextMenuId: 'pf-map-endpoint-contextmenu', // id for "endpoints" context menu
systemContextMenuId: 'pf-map-system-contextmenu', // id for "systems" context menu
contextMenuClass: 'dropdown-menu', // class for all context menus
subMenuLeftClass: 'dropdown-submenu-left', // class moves submenus to the left side
animationInType: 'transition.flipXIn',
animationInDuration: 150,
animationOutType: 'transition.flipXOut',
@@ -29,14 +32,20 @@ define([
*/
let getMenuLeftCoordinate = (e, menuWidth) => {
let mouseWidth = e.pageX;
let pageWidth = $(window).width();
// opening menu would pass the side of the page
if(mouseWidth + menuWidth > pageWidth &&
menuWidth < mouseWidth){
return mouseWidth - menuWidth;
let openSubLeft = false;
if(mouseWidth + menuWidth > window.innerWidth && menuWidth < mouseWidth){
// opening menu would pass the side of the page
openSubLeft = true;
//return mouseWidth - menuWidth;
mouseWidth -= menuWidth;
}else if(mouseWidth + menuWidth * 2 > window.innerWidth && menuWidth * 2 < mouseWidth){
// opening submenu would pass the side of the page
openSubLeft = true;
}
return mouseWidth;
return {
left: mouseWidth,
openSubLeft: openSubLeft
};
};
/**
@@ -47,14 +56,13 @@ define([
*/
let getMenuTopCoordinate = (e, menuHeight) => {
let mouseHeight = e.pageY;
let pageHeight = $(window).height();
// opening menu would pass the bottom of the page
if(mouseHeight + menuHeight > pageHeight &&
menuHeight < mouseHeight){
return mouseHeight - menuHeight;
if(mouseHeight + menuHeight > window.innerHeight && menuHeight < mouseHeight){
// opening menu would pass the bottom of the page
mouseHeight -= menuHeight;
}
return mouseHeight;
return {
top: mouseHeight
};
};
/**
@@ -98,7 +106,7 @@ define([
{icon: 'fa-reply fa-rotate-180', action: 'change_status', text: 'mass status', subitems: [
{subIcon: 'fa-circle', subIconClass: 'txt-color txt-color-gray', subAction: 'status_fresh', subText: 'stage 1 (fresh)'},
{subIcon: 'fa-circle', subIconClass: 'txt-color txt-color-orange', subAction: 'status_reduced', subText: 'stage 2 (reduced)'},
{subIcon: 'fa-circle', subIconClass: 'txt-color txt-color-redDarker', subAction: 'status_critical', subText: 'stage 3 (critical)'}
{subIcon: 'fa-circle', subIconClass: 'txt-color txt-color-redDark', subAction: 'status_critical', subText: 'stage 3 (critical)'}
]},
{icon: 'fa-reply fa-rotate-180', action: 'wh_jump_mass_change', text: 'ship size', subitems: [
@@ -213,7 +221,7 @@ define([
* @param excludeMenu
*/
let closeMenus = excludeMenu => {
let allMenus = $('.dropdown-menu[role="menu"]');
let allMenus = $('.' + config.contextMenuClass + '[role="menu"]');
if(excludeMenu){
allMenus = allMenus.not(excludeMenu);
}
@@ -243,10 +251,13 @@ define([
// hide/activate/disable
menuElement = prepareMenu(menuElement, menuConfig.hidden, menuConfig.active, menuConfig.disabled);
menuElement.css({
let {left, openSubLeft} = getMenuLeftCoordinate(e, menuElement.width());
let {top} = getMenuTopCoordinate(e, menuElement.height());
menuElement.toggleClass(config.subMenuLeftClass, openSubLeft).css({
position: 'absolute',
left: getMenuLeftCoordinate(e, menuElement.width()),
top: getMenuTopCoordinate(e, menuElement.height())
left: left,
top: top
}).velocity(config.animationInType, {
duration: config.animationInDuration,
complete: function(){

View File

@@ -7,8 +7,9 @@ define([
'jquery',
'app/init',
'app/util',
'app/map/util'
], function($, Init, Util, MapUtil){
'app/map/util',
'app/map/overlay/util'
], function($, Init, Util, MapUtil, MapOverlayUtil){
'use strict';
let config = {
@@ -93,6 +94,7 @@ define([
let setOverlayObserver = (overlay, mapId) => {
let overlayMain = overlay.find('.' + config.overlayLocalMainClass);
// open/close toggle ------------------------------------------------------------------------------------------
overlayMain.on('click', function(){
let overlayMain = $(this).parent('.' + config.overlayLocalClass);
let isOpenStatus = isOpen(overlayMain);
@@ -108,6 +110,15 @@ define([
}
});
// trigger table re-draw() ------------------------------------------------------------------------------------
let mapWrapper = overlay.parents('.' + MapUtil.config.mapWrapperClass);
mapWrapper.on('pf:mapResize', function(e){
let tableElement = overlay.find('.' + config.overlayLocalTableClass);
let tableApi = tableElement.DataTable();
tableApi.draw('full-hold');
});
// tooltips ---------------------------------------------------------------------------------------------------
overlayMain.initTooltips({
container: 'body',
placement: 'bottom'
@@ -229,7 +240,7 @@ define([
let indexesRemove = filterRows(localTable, 'id', characterAllIds, false);
localTable.rows(indexesRemove).remove();
localTable.draw();
localTable.draw('full-hold');
// update system relevant data in overlay -----------------------------------------------------------------
updateLocaleHeadline(overlay, systemData, characterAllIds.length, characterLocalIds.length);
@@ -339,12 +350,53 @@ define([
overlay.append(overlayMain);
overlay.append(content);
parentElement.append(overlay);
// set observer
setOverlayObserver(overlay, mapId);
parentElement.append(overlay);
// init local table ---------------------------------------------------------------------------------------
table.on('preDraw.dt', function(e, settings){
let table = $(this);
let mapWrapper = table.parents('.' + MapUtil.config.mapWrapperClass);
// mapWrapper should always exist
if(mapWrapper && mapWrapper.length) {
// check available maxHeight for "locale" table based on current map height (resizable)
let mapHeight = mapWrapper[0].offsetHeight;
let localOverlay = MapOverlayUtil.getMapOverlay(table, 'local');
let paginationElement = localOverlay.find('.dataTables_paginate');
let tableApi = table.DataTable();
let pageInfo = tableApi.page.info();
let localTableRowHeight = 26;
let localTop = localOverlay[0].offsetTop;
let bottomSpace = 38 + 10; // "timer" overlay + some spacing top
bottomSpace += 16 + 5 + 5; // horizontal scrollBar height + some spacing top + bottom
let localHeightMax = mapHeight - bottomSpace - localTop; // max available for local overlay
let localTableBodyMaxHeight = localHeightMax - 53; // - headline height + <thead> height
let newPageLength = Math.floor(localTableBodyMaxHeight / localTableRowHeight);
if(pageInfo.recordsDisplay > newPageLength){
// show pagination and limit page length
localTableBodyMaxHeight -= 30; // - pagination height
newPageLength = Math.floor(localTableBodyMaxHeight / localTableRowHeight);
}
if(pageInfo.length !== newPageLength){
tableApi.page.len(newPageLength);
// page length changed -> show/hide pagination
pageInfo = tableApi.page.info();
if(pageInfo.pages <= 1){
paginationElement.hide();
}else{
paginationElement.show();
}
}
}
});
table.on('draw.dt', function(e, settings){
// init table tooltips
@@ -352,15 +404,6 @@ define([
container: 'body',
placement: 'left'
});
// hide pagination in case of only one page
let paginationElement = overlay.find('.dataTables_paginate');
let pageElements = paginationElement.find('span .paginate_button');
if(pageElements.length <= 1){
paginationElement.hide();
}else{
paginationElement.show();
}
});
// table init complete
@@ -372,9 +415,10 @@ define([
});
});
let localTable = table.DataTable( {
pageLength: 13, // hint: if pagination visible => we need space to show it
let localTable = table.DataTable({
pageLength: 5,
paging: true,
pagingType: 'simple',
lengthChange: false,
ordering: true,
order: [ 0, 'asc' ],
@@ -386,7 +430,11 @@ define([
return 'pf-local-row_' + rowData.id; // characterId
},
language: {
emptyTable: '<span>You&nbsp;are&nbsp;alone</span>'
emptyTable: '<span>You&nbsp;are&nbsp;alone</span>',
paginate: {
next: '&nbsp;',
previous: '&nbsp;'
}
},
columnDefs: [
{
@@ -422,7 +470,7 @@ define([
_: (data, type, row, meta) => {
let value = data.typeName;
if(type === 'display'){
value = '<img src="' + Util.eveImageUrl('render', data.typeId) + '"/>';
value = '<img src="' + Util.eveImageUrl('types', data.typeId) + '"/>';
}
return value;
}

View File

@@ -110,7 +110,7 @@ define([
* @private
*/
let _dragFilter = systemId => {
let filterClasses = ['jtk-drag', 'pf-system-locked'];
let filterClasses = ['jtk-drag', MapUtil.config.systemLockedClass];
return ![...document.getElementById(systemId).classList].some(className => filterClasses.indexOf(className) >= 0);
};

View File

@@ -7,6 +7,8 @@ define([
'app/init',
'app/util',
'app/key',
'app/lib/dragSelect',
'app/lib/eventHandler',
'bootbox',
'app/map/util',
'app/map/contextmenu',
@@ -16,9 +18,8 @@ define([
'app/map/layout',
'app/map/magnetizing',
'app/map/scrollbar',
'dragToSelect',
'app/map/local'
], ($, Init, Util, Key, bootbox, MapUtil, MapContextMenu, MapOverlay, MapOverlayUtil, System, Layout, Magnetizer, Scrollbar) => {
], ($, Init, Util, Key, DragSelect, EventHandler, bootbox, MapUtil, MapContextMenu, MapOverlay, MapOverlayUtil, System, Layout, Magnetizer, Scrollbar) => {
'use strict';
@@ -33,7 +34,6 @@ define([
systemClass: 'pf-system', // class for all systems
systemActiveClass: 'pf-system-active', // class for an active system on a map
systemSelectedClass: 'pf-system-selected', // class for selected systems on a map
systemLockedClass: 'pf-system-locked', // class for locked systems on a map
systemHeadClass: 'pf-system-head', // class for system head
systemHeadNameClass: 'pf-system-head-name', // class for system name
systemHeadCounterClass: 'pf-system-head-counter', // class for system user counter
@@ -644,7 +644,7 @@ define([
case 'add_first_waypoint':
case 'add_last_waypoint':
systemData = system.getSystemData();
Util.setDestination(systemData, action);
Util.setDestination(action, 'system', {id: systemData.systemId, name: systemData.name});
break;
}
};
@@ -1109,6 +1109,8 @@ define([
width = parseInt(width.substring(0, width.length - 2)) || 0;
height = parseInt(height.substring(0, height.length - 2)) || 0;
mapWrapper.trigger('pf:mapResize');
let promiseStore = MapUtil.getLocaleData('map', mapConfig.config.id );
promiseStore.then((data) => {
let storeData = true;
@@ -1988,13 +1990,10 @@ define([
}
dragSystem.find('.' + config.systemHeadNameClass).editable('option', 'placement', placement);
// drag system is not always selected
let selectedSystems = mapContainer.getSelectedSystems().get();
selectedSystems = selectedSystems.concat(dragSystem.get());
selectedSystems = $.unique( selectedSystems );
// repaint connections (and overlays) -> just in case something fails...
revalidate(map, selectedSystems);
// update all dragged systems -> added to DragSelection
params.selection.forEach(elData => {
MapUtil.markAsChanged($(elData[0]));
});
}
});
@@ -2112,7 +2111,7 @@ define([
if( system.data('locked') === true ){
system.data('locked', false);
system.removeClass( config.systemLockedClass );
system.removeClass(MapUtil.config.systemLockedClass);
// enable draggable
map.setDraggable(system, true);
@@ -2122,7 +2121,7 @@ define([
}
}else{
system.data('locked', true);
system.addClass( config.systemLockedClass );
system.addClass(MapUtil.config.systemLockedClass);
// enable draggable
map.setDraggable(system, false);
@@ -2431,11 +2430,17 @@ define([
});
});
// init drag-frame selection
mapContainer.dragToSelect({
selectOnMove: true,
selectables: '.' + config.systemClass,
onHide: function(selectBox, deselectedSystems){
// init drag-frame selection ----------------------------------------------------------------------------------
let dragSelect = new DragSelect({
target: mapContainer[0],
selectables: '.' + config.systemClass + ':not(.' + MapUtil.config.systemLockedClass + '):not(.' + MapUtil.config.systemHiddenClass + ')',
selectedClass: MapUtil.config.systemSelectedClass,
selectBoxClass: 'pf-map-drag-to-select',
boundary: '.mCSB_container_wrapper',
onShow: () => {
Util.triggerMenuAction(document, 'Close');
},
onHide: (deselectedSystems) => {
let selectedSystems = mapContainer.getSelectedSystems();
if(selectedSystems.length > 0){
@@ -2444,23 +2449,19 @@ define([
// convert former group draggable systems so single draggable
for(let i = 0; i < selectedSystems.length; i++){
map.addToDragSelection( selectedSystems[i] );
map.addToDragSelection(selectedSystems[i]);
}
}
// convert former group draggable systems so single draggable
for(let j = 0; j < deselectedSystems.length; j++){
map.removeFromDragSelection( deselectedSystems[j] );
for(let i = 0; i < deselectedSystems.length; i++){
map.removeFromDragSelection(deselectedSystems[i]);
}
},
onShow: function(){
Util.triggerMenuAction(document, 'Close');
},
onRefresh: function(){
}
debug: false,
debugEvents: false
});
// system body expand -----------------------------------------------------------------------------------------
mapContainer.hoverIntent({
over: function(e){
@@ -3177,9 +3178,35 @@ define([
let mapElement = mapWrapper.find('.' + config.mapClass);
let mapId = mapElement.data('id');
let dragSelect;
Scrollbar.initScrollbar(mapWrapper, {
callbacks: {
onInit: function(){
let scrollWrapper = this;
// ++++++++++++++++++++++++++++++++++++++++++++++++++
EventHandler.addEventListener(this, 'update:dragSelect', function(e){
e.stopPropagation();
dragSelect = e.detail;
let intersection = dragSelect.getIntersection();
let originData = dragSelect._selectBox.dataset.origin;
let dragOrigin = originData ? originData.split('|', 2) : [];
let position = [null, null];
let inverseDirection = (directions, i) => directions[((i + 2) % directions.length + directions.length) % directions.length];
let allDirections = ['top', 'right', 'bottom', 'left'];
allDirections.forEach((direction, i, allDirections) => {
if(dragOrigin.includes(direction) && intersection.includes(direction)){
position[i % 2] = direction;
}else if(dragOrigin.includes(direction) && intersection.includes(inverseDirection(allDirections, i))){
// reverse scroll (e.g. 1. drag&select scroll bottom end then move back to top)
position[i % 2] = inverseDirection(allDirections, i);
}
});
Scrollbar.autoScroll(scrollWrapper, position);
}, {capture: true});
// init 'space' key + 'mouse' down for map scroll -------------------------------------------------
let scrollStart = [0, 0];
let mouseStart = [0, 0];
@@ -3263,11 +3290,11 @@ define([
}
};
this.addEventListener('keydown', keyDownHandler, { capture: false });
this.addEventListener('keyup', keyUpHandler, { capture: false });
this.addEventListener('mousemove', mouseMoveHandler, { capture: false });
this.addEventListener('mousedown', mouseDownHandler, { capture: false });
this.addEventListener('mouseup', mouseUpHandler, { capture: false });
this.addEventListener('keydown', keyDownHandler, {capture: false});
this.addEventListener('keyup', keyUpHandler, {capture: false});
this.addEventListener('mousemove', mouseMoveHandler, {capture: false});
this.addEventListener('mousedown', mouseDownHandler, {capture: false});
this.addEventListener('mouseup', mouseUpHandler, {capture: false});
},
onScroll: function(){
// scroll complete
@@ -3287,6 +3314,11 @@ define([
// hide all system head tooltips
$(this).find('.' + config.systemHeadClass + ' .fa').tooltip('hide');
},
whileScrolling: function(){
if(dragSelect){
dragSelect.update();
}
}
}
});

View File

@@ -222,21 +222,6 @@ define([
});
};
/**
* format json object with "time parts" into string
* @param parts
* @returns {string}
*/
let formatTimeParts = parts => {
let label = '';
if(parts.days){
label += parts.days + 'd ';
}
label += ('00' + parts.hours).slice(-2);
label += ':' + ('00' + parts.min).slice(-2);
return label;
};
/**
* hide default icon and replace it with "loading" icon
* @param iconElement
@@ -429,8 +414,8 @@ define([
// format overlay label
let labels = [
formatTimeParts(createdDiff) + '&nbsp;<i class="fas fa-fw fa-plus-square"></i>',
formatTimeParts(updatedDiff) + '&nbsp;<i class="fas fa-fw fa-pen-square"></i>'
Util.formatTimeParts(createdDiff) + '&nbsp;<i class="fas fa-fw fa-plus-square"></i>',
Util.formatTimeParts(updatedDiff) + '&nbsp;<i class="fas fa-fw fa-pen-square"></i>'
];
// add label overlay --------------------------------------------------------------------------
@@ -474,7 +459,7 @@ define([
connection.addOverlay([
'Label',
{
label: '<i class="fas fa-fw fa-hourglass-end"></i>&nbsp;' + formatTimeParts(diff),
label: '<i class="fas fa-fw fa-hourglass-end"></i>&nbsp;' + Util.formatTimeParts(diff),
id: MapOverlayUtil.config.connectionOverlayEolId,
cssClass: [MapOverlayUtil.config.componentOverlayClass, 'eol'].join(' '),
location: 0.25

View File

@@ -5,6 +5,14 @@ define([
], ($) => {
'use strict';
let config = {
autoScrollClass: 'auto-scroll',
autoScrollTopClass: 'auto-scroll-top',
autoScrollLeftClass: 'auto-scroll-left',
autoScrollBottomClass: 'auto-scroll-bottom',
autoScrollRightClass: 'auto-scroll-right',
};
let defaultConfig = {
axis: 'yx',
theme: 'light-3' ,
@@ -18,7 +26,12 @@ define([
callbacks: {
onTotalScrollOffset: 0,
onTotalScrollBackOffset: 0,
alwaysTriggerOffsets: true
alwaysTriggerOffsets: true,
onScroll: function(){
if($(this).data('mCS').trigger === 'internal'){
autoScrollOff(this);
}
}
},
advanced: {
@@ -44,15 +57,99 @@ define([
autoHideScrollbar: false
};
let defaultScrollToOptions = {
scrollInertia: 2000,
scrollEasing: 'easeInOutSmooth',
timeout: 0
};
/**
* init map scrollbar
* @param scrollWrapper
* @param config
* @param customConfig
*/
let initScrollbar = (scrollWrapper, config) => {
config = $.extend(true, {}, defaultConfig, config);
let initScrollbar = (scrollWrapper, customConfig) => {
customConfig = $.extend(true, {}, defaultConfig, customConfig);
// wrap callbacks -> callbacks from defaultConfig should run first
customConfig.callbacks = wrapObjectFunctions(customConfig.callbacks, defaultConfig.callbacks);
scrollWrapper.mCustomScrollbar(customConfig);
};
scrollWrapper.mCustomScrollbar(config);
/**
* @param scrollWrapper
* @param position
* @param options
*/
let autoScroll = (scrollWrapper, position, options) => {
if(position.some(position => position !== null)){
// scroll position -> start auto scroll
autoScrollOn(scrollWrapper, position, options);
}else{
// no scroll position -> stop auto scroll
autoScrollOff(scrollWrapper);
}
};
/**
* @param scrollWrapper
* @param position
* @param options
*/
let autoScrollOn = (scrollWrapper, position, options) => {
let scrollToOptions = Object.assign({}, defaultScrollToOptions, options);
let scrollInertia = 0;
let autoScrollClasses = [];
['top', 'left', 'bottom', 'right'].forEach((direction, i) => {
if(position.includes(direction)){
autoScrollClasses.push(config['autoScroll' + direction.capitalize() + 'Class']);
if(i % 2){ // left || right
scrollInertia = scrollToOptions.scrollInertia * scrollWrapper.mcs.leftPct / 100;
}else{ // top || bottom
scrollInertia = scrollToOptions.scrollInertia * scrollWrapper.mcs.topPct / 100;
}
if(i === 2 || i === 3){ // bottom || right
scrollInertia = scrollToOptions.scrollInertia - scrollInertia;
}
}
});
if(autoScrollClasses.length){
// scroll position -> check if scroll direction changed
let compareClasses = getAutoScrollClasses();
let currentClasses = [...scrollWrapper.classList].filter(cls => compareClasses.includes(cls));
let newClasses = autoScrollClasses.diff(currentClasses);
let oldClasses = currentClasses.diff(autoScrollClasses);
if(newClasses.length || oldClasses.length){
// changed scroll direction (e.g. null -> y; y -> x; y -> xy, xy -> null)
// -> stop current autos scroll and start with new scroll direction
autoScrollOff(scrollWrapper, oldClasses);
scrollWrapper.classList.add(...newClasses);
scrollToOptions.scrollInertia = scrollInertia;
$(scrollWrapper).mCustomScrollbar('scrollTo', position, scrollToOptions);
}
}else{
// no scroll position -> stop auto scroll
autoScrollOff(scrollWrapper);
}
};
/**
* @param scrollWrapper
* @param classes
*/
let autoScrollOff = (scrollWrapper, classes) => {
classes = classes || getAutoScrollClasses();
scrollWrapper.classList.remove(...classes);
$(scrollWrapper).mCustomScrollbar('stop');
};
/**
* @returns {[string, string, string, string]}
*/
let getAutoScrollClasses = () => {
return [config.autoScrollTopClass, config.autoScrollLeftClass, config.autoScrollBottomClass, config.autoScrollRightClass];
};
/**
@@ -174,6 +271,28 @@ define([
*/
let adjustPos = (position, dimension) => mapObject(roundPos(position), (v, k) => Math.max(1, Math.min(dimension[k], v)) );
/**
* wrap functions that exists in both objects (same key)
* -> o2 fkt run 1st (returned value ignored)
* -> o1 fkt run 2nd
* @param o1
* @param o2
* @returns {any}
*/
let wrapObjectFunctions = (o1 = {}, o2 = {}) => {
return mapObject(o1, function(v1, k1){
// check both obj has the same key and are functions
if([v1, o2[k1]].every(v => typeof v === 'function')){
return function(...args){
// run 'default' fkt first, then orig fkt
o2[k1].apply(this, ...args);
return v1.apply(this, ...args);
};
}
return v1;
});
};
/**
* like Array.map() for objects
* -> callback f is called for each property
@@ -187,6 +306,7 @@ define([
return {
initScrollbar: initScrollbar,
scrollToPosition: scrollToPosition,
scrollToCenter: scrollToCenter
scrollToCenter: scrollToCenter,
autoScroll: autoScroll
};
});

View File

@@ -33,6 +33,7 @@ define([
systemClass: 'pf-system', // class for all systems
systemActiveClass: 'pf-system-active', // class for an active system on a map
systemSelectedClass: 'pf-system-selected', // class for selected systems on on map
systemLockedClass: 'pf-system-locked', // class for locked systems on a map
systemHiddenClass: 'pf-system-hidden', // class for hidden (filtered) systems
// dataTable
@@ -530,7 +531,7 @@ define([
// ... get endpoint label for source || target system
if(tmpSystem && tmpSystem){
// ... get all available signature type (wormholes) names
let availableSigTypeNames = SystemSignatures.getAllSignatureNamesBySystem(tmpSystem, 5);
let availableSigTypeNames = SystemSignatures.getSignatureTypeOptionsBySystem(tmpSystem, 5);
let flattenSigTypeNames = Util.flattenXEditableSelectArray(availableSigTypeNames);
if(flattenSigTypeNames.hasOwnProperty(signatureData.typeId)){

View File

@@ -8,8 +8,7 @@ define([
'pnotify.desktop',
//'pnotify.history',
'pnotify.callbacks'
], function($, Init, PNotify){
], ($, Init, PNotify) => {
'use strict';
let config = {
@@ -76,7 +75,7 @@ define([
* @param customConfig
* @param settings
*/
let showNotify = function(customConfig, settings){
let showNotify = (customConfig, settings) => {
customConfig = $.extend(true, {}, config, customConfig );
// desktop notification
@@ -149,7 +148,7 @@ define([
* change document.title and make the browsers tab blink
* @param blinkTitle
*/
let startTabBlink = function(blinkTitle){
let startTabBlink = blinkTitle => {
let initBlink = (function(blinkTitle){
// count blinks if tab is currently active
@@ -174,7 +173,7 @@ define([
blinkTimer = setInterval(blink, 1000);
}
};
}( blinkTitle ));
}(blinkTitle));
initBlink();
};
@@ -182,7 +181,7 @@ define([
/**
* stop blinking document.title
*/
let stopTabBlink = function(){
let stopTabBlink = () => {
if(blinkTimer){
clearInterval(blinkTimer);
document.title = initialPageTitle;

View File

@@ -6,6 +6,7 @@ define([
'jquery',
'app/init',
'app/util',
'app/counter',
'app/logging',
'mustache',
'app/map/util',
@@ -26,7 +27,7 @@ define([
'dialog/credit',
'xEditable',
'app/module_map'
], ($, Init, Util, Logging, Mustache, MapUtil, MapContextMenu, SlideBars, TplHead, TplFooter) => {
], ($, Init, Util, Counter, Logging, Mustache, MapUtil, MapContextMenu, SlideBars, TplHead, TplFooter) => {
'use strict';
@@ -873,13 +874,12 @@ define([
let popoverElement = $(e.target).data('bs.popover').tip();
// destroy all active tooltips inside this popover
popoverElement.destroyTooltip(true);
popoverElement.destroyTooltips(true);
});
// global "modal" callback --------------------------------------------------------------------------------
bodyElement.on('hide.bs.modal', '> .modal', e => {
let modalElement = $(e.target);
modalElement.destroyTimestampCounter(true);
// destroy all form validators
// -> does not work properly. validation functions still used (js error) after 'destroy'
@@ -892,6 +892,14 @@ define([
modalElement.find('.' + Util.config.select2Class)
.filter((i, element) => $(element).data('select2'))
.select2('destroy');
// destroy DataTable instances
for(let table of modalElement.find('table.dataTable')){
$(table).DataTable().destroy(true);
}
// destroy counter
Counter.destroyTimestampCounter(modalElement, true);
});
// global "close" trigger for context menus ---------------------------------------------------------------
@@ -1060,7 +1068,7 @@ define([
if(changedCharacter){
// current character changed
userInfoElement.find('span').text(Util.getObjVal(userData, 'character.name'));
userInfoElement.find('img').attr('src', Util.eveImageUrl('character', Util.getObjVal(userData, 'character.id')));
userInfoElement.find('img').attr('src', Util.eveImageUrl('characters', Util.getObjVal(userData, 'character.id')));
}
// init "character switch" popover
userInfoElement.initCharacterSwitchPopover(userData);
@@ -1145,7 +1153,7 @@ define([
if(isCurrentLocation && shipTypeId){
// show ship image
breadcrumbHtml += '<img class="pf-head-image --right" ';
breadcrumbHtml += 'src="' + Util.eveImageUrl('render', shipTypeId) + '" ';
breadcrumbHtml += 'src="' + Util.eveImageUrl('types', shipTypeId) + '" ';
breadcrumbHtml += 'title="' + shipTypeName + '" ';
breadcrumbHtml += '>';
}

View File

@@ -43,7 +43,6 @@ define([], () => {
constructor(callback, timeout = 6000){
let timer;
let promise = callback[Symbol.toStringTag] === 'Promise' ? callback : new Promise(callback);
//let promise = new Promise(callback);
let wrapperPromise = Promise.race([
promise,
@@ -63,7 +62,6 @@ define([], () => {
reject(error);
});
});
}
};
});

View File

@@ -6,9 +6,10 @@ define([
'jquery',
'app/init',
'app/util',
'app/render',
'app/map/worker',
'mustache',
], function($, Init, Util, MapWorker, Mustache){
'peityInlineChart',
], ($, Init, Util, Render, MapWorker) => {
'use strict';
let config = {
@@ -17,8 +18,20 @@ define([
// navigation
navigationElementId: 'pf-navbar', // id for navbar element
// sticky panel
stickyPanelClass: 'pf-landing-sticky-panel', // class for sticky panels
hiddenByAttributeClass: 'pf-hidden-by-attr', // class for elements that are hidden/shown by [data-attr] value
shownByAttributeClass: 'pf-shown-by-attr', // class for elements that are hidden/shown by [data-attr] value
webSocketStatsId: 'pf-setup-webSocket-stats', // id for webSocket "stats" panel
webSocketRefreshStatsId: 'pf-setup-webSocket-stats-refresh' // class for "reload stats" button
webSocketRefreshStatsId: 'pf-setup-webSocket-stats-refresh', // class for "reload stats" button
cronRowActiveClass: 'pf-cron-row-active', // class for "active" (e.g. "inProgress") table row
jsonPopoverClass: 'pf-json-popover', // class for "json" popover elements
barChartClass: 'pf-bar-chart', // class for "bar" chart elements
lineChartClass: 'pf-line-chart' // class for "line" chart elements
};
/**
@@ -75,35 +88,96 @@ define([
// collapse ---------------------------------------------------------------------------------------------------
setCollapseObserver(body, '[data-toggle="collapse"]');
// panel actions ----------------------------------------------------------------------------------------------
let collapsedState = false;
$('.' + config.stickyPanelClass + ' a[data-panel-action="collapse-all"]').on('click', function(e){
e.preventDefault();
$('.row.collapse').collapse(collapsedState ? 'show' : 'hide');
collapsedState = !collapsedState;
});
// buttons ----------------------------------------------------------------------------------------------------
// exclude "download" && "navigation" buttons
body.find('.btn')
.not('.navbar-fixed-bottom .btn')
.not('[data-action="clearIndex"]')
.not('[data-action="buildIndex"]')
.not('[href^="?export"]').on('click', function(e){
$('.' + config.splashOverlayClass).showSplashOverlay();
});
.not('[data-action]')
.not('[href^="?export"]')
.on('click', function(){
$('.' + config.splashOverlayClass).showSplashOverlay();
});
// build/clear index buttons ----------------------------------------------------------------------------------
// clear index buttons ----------------------------------------------------------------------------------------
body.find('.btn[data-action="buildIndex"], .btn[data-action="clearIndex"]').on('click', function(e){
body.on('click', '.btn[data-action]', function(e){
e.preventDefault();
e.stopPropagation();
let element = $(this);
let url = '/api/setup/' + element.attr('data-action');
sendRequest(url, {
type: element.attr('data-type'),
countAll: element.attr('data-countall'),
count: 0,
offset: 0
}, {
let payload = element.attr('data-payload');
let callStartName = element.attr('data-callstart');
let callBackName = element.attr('data-callback');
let callBack = config[callBackName] instanceof Function ? config[callBackName] : () => {
console.warn('Invalid callback function name: %s', callBackName);
};
let requestData = {};
try{
requestData = Object.assign({}, requestData, JSON.parse(payload));
}catch(error){
console.error('Failed to parse button payload: %s ', payload);
}
let context = {
target: element,
url: url
}, updateIndexCount);
};
if(config[callStartName] instanceof Function){
config[callStartName](context);
}
sendRequest(url, requestData, context, callBack);
});
// bar charts (e.g. cronjob table) ----------------------------------------------------------------------------
initBarCharts($('.' + config.barChartClass));
// Json popovers ----------------------------------------------------------------------------------------------
body.on('mouseenter', '.' + config.jsonPopoverClass, function(e){
let element = $(this);
if(!element.data('bs.popover')){
let json = element.data('json');
let jsonHighlighted = Render.highlightJson(json);
let content = '<pre><code>' + jsonHighlighted + '</code></pre>';
element.popover({
placement: 'left',
html: true,
trigger: 'hover',
content: content,
container: 'body',
title: 'Last exec. state',
delay: {
show: 180,
hide: 0
}
});
element.popover('show');
}
});
// tooltips ---------------------------------------------------------------------------------------------------
body.find('[title]').tooltip();
body.initTooltips({
container: 'body'
});
body.on('show.bs.tooltip', e => {
let element = $(e.target);
let level = element.attr('data-level');
if(level && level.length){
element.data('bs.tooltip').$tip.find('.tooltip-inner').addClass('txt-color txt-color-' + level);
}
});
// change url (remove logout parameter)
if(history.pushState){
@@ -111,12 +185,150 @@ define([
}
};
/**
* init "Peity" charts
* @param elements
* @param options
*/
let initBarCharts = (elements, options = {}) => {
let barDangerColor = '#a52521';
let barWarningColor = '#e28a0d';
let barDefaultColor = '#568a89';
// defaultOptions for bar AND line charts
let defaultOptions = {
fill: function(val, i, all){
if(val <= 0){
return barDangerColor;
}else{
// fade out older bars
let opacity = i / all.length;
// limit opacity to min 0.3
opacity = Math.max(opacity, 0.3);
let alphaHex = Math.round(opacity * 255).toString(16);
// set color based on average difference
let avg = this.$el.data('avg') || 0;
let avgBuffer = this.$el.data('avg-buffer') || 0;
let barColor = (val > avg + avgBuffer ) ? barWarningColor : barDefaultColor;
return barColor + (alphaHex.length === 1 ? '0' : '') + alphaHex;
}
},
height: 18,
min: -1,
//max: 2
//width: '100%'
width: '65px'
};
elements.peity('bar', Object.assign({}, defaultOptions, {
padding: 0.1 // bar chart specific
}, options));
};
/**
* @param container
*/
let updateStatusBar = container => {
let statusConfig = ['information', 'hint', 'warning', 'danger', 'success'].map(
type => ({
type: type,
target: container.find('.' + config.hiddenByAttributeClass + ', .' + config.shownByAttributeClass)
.filter('[data-attr-type="' + type + '"]'),
count: container.find('[data-type="' + type + '"]').length
})
);
// show/hide target elements by CSS by attribute update
let typesCheckForSuccess = ['warning', 'danger'];
let checkCount = 0;
for(let config of statusConfig){
// "success" type depends on empty count for checked types
if(typesCheckForSuccess.includes(config.type)){
checkCount += config.count;
}
let count = config.type === 'success' ? checkCount : config.count;
config.target.attr('data-attr', count || '');
}
};
/**
* update cronJob table row
* @type {updateCronjob}
*/
let updateCronjob = config.updateCronjob = (context, responseData) => {
context.target.button('reset');
let jobsData = Util.getObjVal(responseData, 'jobsData') || {};
let html = Util.getObjVal(responseData, 'html');
let jobsCount = Object.keys(jobsData).length;
if(jobsCount && html){
// replace full table tbody or just a single row
let elPanel = context.target.closest('.panel');
let elOld;
let elNew;
if(jobsCount === 1){
// replace single: <tr>
elOld = context.target.closest('tr');
elNew = $(html);
}else{
// replace all: <tbody>
elOld = context.target.closest('table').find('tbody');
elNew = $('<tbody>' + html + '</tbody>');
}
if(elOld && elNew){
elOld.destroyTooltips(true);
elOld.replaceWith(elNew);
elNew.initTooltips({
container: 'body'
});
initBarCharts(elNew.find('.' + config.barChartClass));
updateStatusBar(elPanel);
}
}
};
/**
* mark cronJob table row + status icons as "inProgress"
* @type {startCronjob}
*/
let startCronjob = config.startCronjob = context => {
let row = context.target.closest('tr');
let statusCell = row.children().first().children().first();
// change row to "inProgress"
row.addClass(config.cronRowActiveClass);
// change status icons to "inProgress"
let mapAttr = path => '[data-name="' + path + '"]';
let removeStatusNames = ['notExecuted', 'notFinished', 'onHold'];
statusCell.find(removeStatusNames.map(mapAttr).join(', ')).remove();
let addStatusName = 'inProgress';
let addStatusType = 'success';
if(!statusCell.find([addStatusName].map(mapAttr).join(', ')).length){
statusCell.append(
$('<i>', {
class: 'fas fa-fw fa-play pf-help txt-color txt-color-success'
})
.attr('title', 'Started. In execution…')
.attr('data-name', addStatusName)
.attr('data-type', addStatusType)
).initTooltips({container: 'body'});
}
};
/**
* update data count label for "indexed data"
* @param context
* @param responseData
*/
let updateIndexCount = (context, responseData) => {
let updateIndexCount = config.updateIndexCount = (context, responseData) => {
let countElement = context.target.closest('tr').children().eq(1).find('kbd');
countElement.text(responseData.countBuildAll + '/' + responseData.countAll);
countElement.removeClass('txt-color-success txt-color-danger txt-color-warning');

View File

@@ -7,10 +7,10 @@ define([
'app/init',
'app/util',
'app/render',
'bootbox',
'app/counter',
'bootbox',
'app/map/util'
], ($, Init, Util, Render, bootbox, Counter, MapUtil) => {
], ($, Init, Util, Render, Counter, bootbox, MapUtil) => {
'use strict';
@@ -182,7 +182,7 @@ define([
mapElement.append(dlElementRight);
// init map lifetime counter
$('.' + config.mapInfoLifetimeCounterClass).initTimestampCounter();
Counter.initTimestampCounter($('.' + config.mapInfoLifetimeCounterClass));
mapElement.find('.' + config.textActionIconCopyClass).on('click', function(){
let mapUrl = $(this).find('span').text().trim();
@@ -456,7 +456,7 @@ define([
width: 10,
className: ['text-center', config.tableCellActionClass].join(' '),
data: null,
defaultContent: '<i class="fas fa-times txt-color txt-color-redDarker"></i>',
defaultContent: '<i class="fas fa-times txt-color txt-color-redDark"></i>',
createdCell: function(cell, cellData, rowData, rowIndex, colIndex){
let tempTableElement = this;
@@ -657,7 +657,7 @@ define([
width: 10,
className: ['text-center', config.tableCellActionClass].join(' '),
data: null,
defaultContent: '<i class="fas fa-times txt-color txt-color-redDarker"></i>',
defaultContent: '<i class="fas fa-times txt-color txt-color-redDark"></i>',
createdCell: function(cell, cellData, rowData, rowIndex, colIndex){
let tempTableElement = this;
@@ -764,7 +764,7 @@ define([
_: function(data, type, row, meta){
let value = data;
if(data && type === 'display'){
value = '<img src="' + Util.eveImageUrl('render', value.typeId) + '" title="' + value.typeName + '" data-toggle="tooltip" />';
value = '<img src="' + Util.eveImageUrl('types', value.typeId) + '" title="' + value.typeName + '" data-toggle="tooltip" />';
}
return value;
}
@@ -802,7 +802,7 @@ define([
_: function(data, type, row, meta){
let value = data;
if(type === 'display'){
value = '<img src="' + Util.eveImageUrl('character', value) + '"/>';
value = '<img src="' + Util.eveImageUrl('characters', value) + '"/>';
}
return value;
}
@@ -842,7 +842,7 @@ define([
_: function(data, type, row, meta){
let value = data;
if(type === 'display'){
value = '<img src="' + Util.eveImageUrl('corporation', value.id) + '"/>';
value = '<img src="' + Util.eveImageUrl('corporations', value.id) + '"/>';
}
return value;
}
@@ -1125,7 +1125,7 @@ define([
_: function(data, type, row, meta){
let value = data;
if(type === 'display'){
value = '<img src="' + Util.eveImageUrl('character', value) + '"/>';
value = '<img src="' + Util.eveImageUrl('characters', value) + '"/>';
}
return value;
}
@@ -1167,7 +1167,6 @@ define([
_: function(data, type, row, meta){
let value = data;
if(type === 'display'){
// txt-color-redDarker
value = '<i class="fas fa-code ' + config.tableCellActionIconClass + '"></i>';
}
return value;
@@ -1250,7 +1249,7 @@ define([
class: config.tableToolsClass
}));
let buttons = new $.fn.dataTable.Buttons( logDataTable, {
let buttons = new $.fn.dataTable.Buttons(logDataTable, {
buttons: [
{
className: 'btn btn-sm btn-default',

View File

@@ -8,9 +8,10 @@ define([
'app/init',
'app/util',
'app/render',
'app/counter',
'bootbox',
'peityInlineChart'
], ($, Init, Util, Render, bootbox) => {
], ($, Init, Util, Render, Counter, bootbox) => {
'use strict';
let config = {
@@ -37,7 +38,7 @@ define([
* init blank statistics dataTable
* @param dialogElement
*/
let initStatsTable = function(dialogElement){
let initStatsTable = dialogElement => {
let columnNumberWidth = 28;
let cellPadding = 4;
let lineChartWidth = columnNumberWidth + (2 * cellPadding);
@@ -114,6 +115,7 @@ define([
columnDefs: [
{
targets: 0,
name: 'rowIndex',
title: '<i class="fas fa-hashtag"></i>',
orderable: false,
searchable: false,
@@ -122,6 +124,7 @@ define([
data: 'character.id'
},{
targets: 1,
name: 'image',
title: '',
orderable: false,
searchable: false,
@@ -130,11 +133,12 @@ define([
data: 'character',
render: {
_: function(data, type, row, meta){
return '<img src="' + Util.eveImageUrl('character', parseInt(data.id)) + '"/>';
return '<img src="' + Util.eveImageUrl('characters', parseInt(data.id)) + '"/>';
}
}
},{
targets: 2,
name: 'name',
title: 'name',
width: 200,
data: 'character',
@@ -144,20 +148,16 @@ define([
}
},{
targets: 3,
name: 'lastLogin',
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();
}
data: 'character.lastLogin',
defaultContent: ''
},{
targets: 4,
name: 'mapCreate',
title: '<span title="created" data-toggle="tooltip">C&nbsp;&nbsp;</span>',
orderable: false,
searchable: false,
@@ -169,6 +169,7 @@ define([
}
},{
targets: 5,
name: 'mapUpdate',
title: '<span title="updated" data-toggle="tooltip">U&nbsp;&nbsp;</span>',
orderable: false,
searchable: false,
@@ -180,6 +181,7 @@ define([
}
},{
targets: 6,
name: 'mapDelete',
title: '<span title="deleted" data-toggle="tooltip">D&nbsp;&nbsp;</span>',
orderable: false,
searchable: false,
@@ -191,6 +193,7 @@ define([
}
},{
targets: 7,
name: 'mapSum',
title: 'Σ&nbsp;&nbsp;',
searchable: false,
width: 20,
@@ -201,6 +204,7 @@ define([
}
},{
targets: 8,
name: 'systemCreate',
title: '<span title="created" data-toggle="tooltip">C&nbsp;&nbsp;</span>',
orderable: false,
searchable: false,
@@ -212,6 +216,7 @@ define([
}
},{
targets: 9,
name: 'systemUpdate',
title: '<span title="updated" data-toggle="tooltip">U&nbsp;&nbsp;</span>',
orderable: false,
searchable: false,
@@ -223,6 +228,7 @@ define([
}
},{
targets: 10,
name: 'systemDelete',
title: '<span title="deleted" data-toggle="tooltip">D&nbsp;&nbsp;</span>',
orderable: false,
searchable: false,
@@ -234,6 +240,7 @@ define([
}
},{
targets: 11,
name: 'systemSum',
title: 'Σ&nbsp;&nbsp;',
searchable: false,
width: 20,
@@ -244,6 +251,7 @@ define([
}
},{
targets: 12,
name: 'connectionCreate',
title: '<span title="created" data-toggle="tooltip">C&nbsp;&nbsp;</span>',
orderable: false,
searchable: false,
@@ -255,6 +263,7 @@ define([
}
},{
targets: 13,
name: 'connectionUpdate',
title: '<span title="updated" data-toggle="tooltip">U&nbsp;&nbsp;</span>',
orderable: false,
searchable: false,
@@ -266,6 +275,7 @@ define([
}
},{
targets: 14,
name: 'connectionDelete',
title: '<span title="deleted" data-toggle="tooltip">D&nbsp;&nbsp;</span>',
orderable: false,
searchable: false,
@@ -277,6 +287,7 @@ define([
}
},{
targets: 15,
name: 'connectionSum',
title: 'Σ&nbsp;&nbsp;',
searchable: false,
width: 20,
@@ -287,6 +298,7 @@ define([
}
},{
targets: 16,
name: 'signatureCreate',
title: '<span title="created" data-toggle="tooltip">C&nbsp;&nbsp;</span>',
orderable: false,
searchable: false,
@@ -298,6 +310,7 @@ define([
}
},{
targets: 17,
name: 'signatureUpdate',
title: '<span title="updated" data-toggle="tooltip">U&nbsp;&nbsp;</span>',
orderable: false,
searchable: false,
@@ -309,6 +322,7 @@ define([
}
},{
targets: 18,
name: 'signatureDelete',
title: '<span title="deleted" data-toggle="tooltip">D&nbsp;&nbsp;</span>',
orderable: false,
searchable: false,
@@ -320,6 +334,7 @@ define([
}
},{
targets: 19,
name: 'signatureSum',
title: 'Σ&nbsp;&nbsp;',
searchable: false,
width: 20,
@@ -330,6 +345,7 @@ define([
}
},{
targets: 20,
name: 'totalSum',
title: 'Σ&nbsp;&nbsp;',
searchable: false,
width: 20,
@@ -346,6 +362,8 @@ define([
// initial statistics data request
let requestData = getRequestDataFromTabPanels(dialogElement);
getStatsData(requestData, {tableApi: tableApi, callback: drawStatsTable});
Counter.initTableCounter(this, ['lastLogin:name']);
},
drawCallback: function(settings){
this.api().rows().nodes().to$().each(function(i, row){
@@ -374,7 +392,7 @@ define([
});
$(sumColumnIndexes).each(function(index, value){
$( api.column( value ).footer() ).text( renderNumericColumn(pageTotalColumns[index], 'display') );
$(api.column(value).footer()).text( renderNumericColumn(pageTotalColumns[index], 'display') );
});
},
data: [] // will be added dynamic
@@ -404,8 +422,7 @@ define([
* @param requestData
* @param context
*/
let getStatsData = function(requestData, context){
let getStatsData = (requestData, context) => {
context.dynamicArea = $('#' + config.statsContainerId + ' .' + Util.config.dynamicAreaClass);
context.dynamicArea.showLoadingAnimation();
@@ -418,7 +435,7 @@ define([
}).done(function(data){
this.dynamicArea.hideLoadingAnimation();
this.callback(data);
this.callback(data, this);
}).fail(function(jqXHR, status, error){
let reason = status + ' ' + error;
Util.showNotify({title: jqXHR.status + ': loadStatistics', text: reason, type: 'warning'});
@@ -430,7 +447,7 @@ define([
* update "header"/"filter" elements in dialog
* @param responseData
*/
let drawStatsTable = function(responseData){
let drawStatsTable = (responseData, context) => {
let dialogElement = $('#' + config.statsDialogId);
// update filter/header -----------------------------------------------------------------------------
@@ -467,8 +484,8 @@ define([
// clear and (re)-fill table ------------------------------------------------------------------------
let formattedData = formatStatisticsData(responseData);
this.tableApi.clear();
this.tableApi.rows.add(formattedData).draw();
context.tableApi.clear();
context.tableApi.rows.add(formattedData).draw();
};
/**
@@ -477,7 +494,7 @@ define([
* @param statsData
* @returns {Array}
*/
let formatStatisticsData = function(statsData){
let formatStatisticsData = statsData => {
let formattedData = [];
let yearStart = statsData.start.year;
let weekStart = statsData.start.week;
@@ -687,7 +704,7 @@ define([
* @param dialogElement
* @returns {{}}
*/
let getRequestDataFromTabPanels = function(dialogElement){
let getRequestDataFromTabPanels = dialogElement => {
let requestData = {};
// get data from "tab" panel links ------------------------------------------------------------------

View File

@@ -38,19 +38,19 @@ define([
switch(data.categoryType){
case 'character':
imagePath = Util.eveImageUrl('character', data.id);
imagePath = Util.eveImageUrl('characters', data.id);
break;
case 'corporation':
imagePath = Util.eveImageUrl('corporation', data.id);
imagePath = Util.eveImageUrl('corporations', data.id);
break;
case 'alliance':
imagePath = Util.eveImageUrl('alliance', data.id);
imagePath = Util.eveImageUrl('alliances', data.id);
break;
case 'inventoryType':
imagePath = Util.eveImageUrl('type', data.id);
imagePath = Util.eveImageUrl('types', data.id);
break;
case 'render':
imagePath = Util.eveImageUrl('render', data.id);
imagePath = Util.eveImageUrl('types', data.id);
break;
case 'station':
iconName = 'fa-home';
@@ -77,14 +77,23 @@ define([
/**
* format results data for signature type select
* @param state
* @returns {*|jQuery|HTMLElement}
* @param container
* @param customOptions
* @returns {*|k.fn.init|jQuery|HTMLElement}
*/
let formatSignatureTypeSelectionData = state => {
let formatSignatureTypeSelectionData = (state, container, customOptions) => {
let parts = state.text.split(' - ');
let markup = '';
if(parts.length === 2){
// wormhole data -> 2 columns
let name = parts[0];
let sizeLabel;
if(Util.getObjVal(customOptions, 'showWhSizeLabel')){
let wormholeSizeData = Util.getObjVal(Init, 'wormholes.' + name + '.size');
sizeLabel = Util.getObjVal(wormholeSizeData, 'label') || '';
}
let securityClass = Util.getSecurityClassForSystem(getSystemSecurityFromLabel(parts[1]));
// some labels have a "suffix" label that should not have the securityClass
let labelParts = parts[1].split(/\s(.+)/);
@@ -93,9 +102,14 @@ define([
let classes = [securityClass, Util.config.popoverTriggerClass, Util.config.helpDefaultClass];
markup += '<span>' + parts[0] + '</span>&nbsp;&nbsp;';
markup += '<span>' + name + '</span>';
if(sizeLabel !== undefined){
markup += '<span><kbd>' + sizeLabel + '</kbd></span>';
}else{
markup += '&nbsp;&nbsp;';
}
markup += '<i class="fas fa-long-arrow-alt-right txt-color txt-color-grayLight"></i>';
markup += '<span class="' + classes.join(' ') + '" data-name="' + parts[0] + '">&nbsp;&nbsp;' + label + '</span>';
markup += '<span class="' + classes.join(' ') + '" data-name="' + name + '">&nbsp;&nbsp;' + label + '</span>';
if(suffix.length){
markup += '&nbsp;<span>' + suffix + '</span>';
}
@@ -343,15 +357,15 @@ define([
let hideShatteredClass = !data.shattered ? 'hide' : '';
let markup = '<div class="clearfix ' + config.resultOptionImageClass + '">';
markup += '<div class="col-sm-4 pf-select-item-anchor ' + systemNameClass + '">' + data.text + '</div>';
markup += '<div class="col-sm-2 text-right ' + data.effectClass + '">';
markup += '<div class="col-xs-4 pf-select-item-anchor ' + systemNameClass + '">' + data.text + '</div>';
markup += '<div class="col-xs-2 text-right ' + data.effectClass + '">';
markup += '<i class="fas fa-fw fa-square ' + hideEffectClass + '"></i>';
markup += '</div>';
markup += '<div class="col-sm-2 text-right ' + data.secClass + '">' + data.security + '</div>';
markup += '<div class="col-sm-2 text-right ' + shatteredClass + '">';
markup += '<div class="col-xs-2 text-right ' + data.secClass + '">' + data.security + '</div>';
markup += '<div class="col-xs-2 text-right ' + shatteredClass + '">';
markup += '<i class="fas fa-fw fa-chart-pie ' + hideShatteredClass + '"></i>';
markup += '</div>';
markup += '<div class="col-sm-2 text-right ' + data.trueSecClass + '">' + data.trueSec + '</div></div>';
markup += '<div class="col-xs-2 text-right ' + data.trueSecClass + '">' + data.trueSec + '</div></div>';
return markup;
}

View File

@@ -7,8 +7,9 @@ define([
'app/init',
'app/util',
'bootbox',
'app/counter',
'app/map/util'
], ($, Init, Util, bootbox, MapUtil) => {
], ($, Init, Util, bootbox, Counter, MapUtil) => {
'use strict';
let config = {
@@ -774,7 +775,7 @@ define([
_: function(data, type, row){
let value = data.typeId;
if(type === 'display'){
value = '<img src="' + Util.eveImageUrl('render', value) + '" title="' + data.typeName + '" data-toggle="tooltip" />';
value = '<img src="' + Util.eveImageUrl('types', value) + '" title="' + data.typeName + '" data-toggle="tooltip" />';
}
return value;
}
@@ -794,7 +795,7 @@ define([
_: (cellData, type, rowData, meta) => {
let value = cellData.name;
if(type === 'display'){
value = '<img src="' + Util.eveImageUrl('character', cellData.id) + '" title="' + value + '" data-toggle="tooltip" />';
value = '<img src="' + Util.eveImageUrl('characters', cellData.id) + '" title="' + value + '" data-toggle="tooltip" />';
}
return value;
}
@@ -827,10 +828,7 @@ define([
title: 'log',
width: 55,
className: ['text-right', config.tableCellCounterClass].join(' '),
data: 'created.created',
createdCell: function(cell, cellData, rowData, rowIndex, colIndex){
$(cell).initTimestampCounter('d');
}
data: 'created.created'
},{
targets: 5,
name: 'edit',
@@ -873,7 +871,7 @@ define([
display: data => {
let val = '<i class="fas fa-plus"></i>';
if(data){
val = '<i class="fas fa-times txt-color txt-color-redDarker"></i>';
val = '<i class="fas fa-times txt-color txt-color-redDark"></i>';
}
return val;
}
@@ -931,6 +929,9 @@ define([
}
}
],
initComplete: function(settings, json){
Counter.initTableCounter(this, ['created:name']);
},
drawCallback: function(settings){
let animationRows = this.api().rows().nodes().to$().filter(function(a,b ){
return (

View File

@@ -29,7 +29,8 @@ define([
ykeys: ['y'],
labels: ['Jumps'],
lineColors: ['#375959'],
pointFillColors: ['#477372']
pointFillColors: ['#477372'],
infoLabels: ['Avg. jumps']
},
shipKills: {
headline: 'Ship/POD Kills',
@@ -37,7 +38,8 @@ define([
ykeys: ['y', 'z'],
labels: ['Ships', 'PODs'],
lineColors: ['#375959', '#477372'],
pointFillColors: ['#477372', '#568a89']
pointFillColors: ['#477372', '#568a89'],
infoLabels: ['Avg. ship kills', 'Avg. pod kills']
},
factionKills: {
headline: 'NPC Kills',
@@ -45,7 +47,8 @@ define([
ykeys: ['y'],
labels: ['NPCs'],
lineColors: ['#375959'],
pointFillColors: ['#477372']
pointFillColors: ['#477372'],
infoLabels: ['Avg. NPC kills']
}
}
};
@@ -69,8 +72,11 @@ define([
* @param graphKey
* @param graphData
* @param eventLine
* @returns {Array|null}
*/
let initGraph = (graphElement, graphKey, graphData, eventLine) => {
let tooltipData = null;
if(
graphData.logExists &&
graphData.data &&
@@ -100,6 +106,7 @@ define([
// ... calc average
let goals = Object.values(sum).map(value => Math.floor(value / dataLength));
// init Morris chart --------------------------------------------------------------------------------------
let graphConfig = {
element: graphElement,
data: graphData.data,
@@ -111,11 +118,11 @@ define([
yLabelFormat: value => Math.round(value),
padding: 8,
hideHover: true,
pointSize: 3,
pointSize: 2.5,
lineColors: getInfoForGraph(graphKey, 'lineColors'),
pointFillColors: getInfoForGraph(graphKey, 'pointFillColors'),
pointStrokeColors: ['#141519'],
lineWidth: 2,
lineWidth: 1.5,
grid: true,
gridStrokeWidth: 0.3,
gridTextSize: 9,
@@ -125,7 +132,7 @@ define([
goals: goals,
goalStrokeWidth: 1,
goalLineColors: ['#c2760c'],
smooth: true,
smooth: false,
fillOpacity: 0.2,
resize: true,
redraw: true,
@@ -133,15 +140,36 @@ define([
eventLineColors: ['#63676a']
};
if(eventLine >= 0){
if(eventLine > 0){
graphConfig.events = [eventLine];
}
Morris.Area(graphConfig);
// data for info "popover" --------------------------------------------------------------------------------
tooltipData = {};
let tooltipRows = [];
let infoLabels = getInfoForGraph(graphKey, 'infoLabels');
goals.forEach((goal, i) => {
tooltipRows.push({
label: infoLabels[i],
value: goal,
class: 'txt-color txt-color-orangeDark'
});
});
tooltipData.rows = tooltipRows;
let serverDate = Util.getServerTime();
let updatedDate = Util.convertTimestampToServerTime(graphData.updated);
let updatedDiff = Util.getTimeDiffParts(updatedDate, serverDate);
tooltipData.title = '<i class="fas fa-download"></i><span class="pull-right ">' + Util.formatTimeParts(updatedDiff) + '</span>';
}else{
// make container a bit smaller -> no graph shown
graphElement.css('height', '22px').text('No data');
}
return tooltipData;
};
/**
@@ -190,7 +218,7 @@ define([
*/
let addGraphData = (context, graphData) => {
// calculate time offset until system created -----------------------------------------------------------------
// calculate time offset until system updated -----------------------------------------------------------------
let serverData = Util.getServerTime();
let timestampNow = Math.floor(serverData.getTime() / 1000);
let timeSinceUpdate = timestampNow - context.systemData.updated.updated;
@@ -198,19 +226,87 @@ define([
let timeInHours = Math.floor(timeSinceUpdate / 3600);
let timeInMinutes = Math.floor((timeSinceUpdate % 3600) / 60);
let timeInMinutesPercent = parseFloat((timeInMinutes / 60).toFixed(2));
let eventLine = timeInHours * timeInMinutesPercent;
// graph is from right to left -> convert event line
eventLine = 23 - eventLine;
let eventLine = Math.max(parseFloat((24 - timeInHours - timeInMinutesPercent).toFixed(2)), 0);
// update graph data ------------------------------------------------------------------------------------------
for(let [systemId, graphsData] of Object.entries(graphData)){
for(let [graphKey, graphData] of Object.entries(graphsData)){
let graphElement = context.moduleElement.find('[data-graph="' + graphKey + '"]');
let graphColElement = context.moduleElement.find('[data-graph="' + graphKey + '"]');
let graphElement = graphColElement.find('.' + config.systemGraphClass);
graphElement.hideLoadingAnimation();
initGraph(graphElement, graphKey, graphData, eventLine);
graphColElement.data('infoData', initGraph(graphElement, graphKey, graphData, eventLine));
}
}
setModuleObserver(context.moduleElement);
};
/**
* @param moduleElement
*/
let setModuleObserver = moduleElement => {
moduleElement.hoverIntent({
over: function(e){
let element = $(this);
let tooltipData = element.parents('[data-graph]').data('infoData');
if(tooltipData){
element.addSystemGraphTooltip(tooltipData.rows, {
trigger: 'manual',
title: tooltipData.title
}).popover('show');
}
},
out: function(e){
$(this).destroyPopover();
},
selector: '.' + Util.config.popoverTriggerClass
});
};
/**
* add info tooltip for graphs
* @param tooltipData
* @param options
* @returns {void|*|undefined}
*/
$.fn.addSystemGraphTooltip = function(tooltipData, options = {}){
let table = '<table>';
for(let data of tooltipData){
let css = data.class || '';
table += '<tr>';
table += '<td>';
table += data.label;
table += '</td>';
table += '<td class="text-right ' + css + '">';
table += data.value;
table += '</td>';
table += '</tr>';
}
table += '</table>';
let defaultOptions = {
placement: 'top',
html: true,
trigger: 'hover',
container: 'body',
title: 'Info',
content: table,
delay: {
show: 0,
hide: 0
},
};
options = $.extend({}, defaultOptions, options);
return this.each(function(){
$(this).popover(options);
});
};
/**
@@ -262,11 +358,11 @@ define([
class: 'row'
});
for(let [graphKey, graphConfig] of Object.entries(config.systemGraphs)){
for(let graphKey of Object.keys(config.systemGraphs)){
rowElement.append(
$('<div>', {
class: ['col-xs-12', 'col-sm-4'].join(' ')
}).append(
}).attr('data-graph', graphKey).append(
$('<div>', {
class: config.moduleHeadClass
}).append(
@@ -275,11 +371,18 @@ define([
}),
$('<h5>', {
text: getInfoForGraph(graphKey, 'headline')
})
}),
$('<h5>', {
class: 'pull-right'
}).append(
$('<small>', {
html: '<i class="fas fa-fw fa-question-circle pf-help ' + Util.config.popoverTriggerClass + '"></i>'
})
)
),
$('<div>', {
class: config.systemGraphClass
}).attr('data-graph', graphKey)
})
)
);
}

View File

@@ -20,12 +20,12 @@ define([
// headline toolbar
moduleHeadlineIconClass: 'pf-module-icon-button', // class for toolbar icons in the head
textActionIconCopyClass: 'pf-module-icon-button-copy', // class for text action "copy"
// breadcrumb
constellationLinkClass: 'pf-system-info-constellation', // class for "constellation" name
regionLinkClass: 'pf-system-info-region', // class for "region" name
typeLinkClass: 'pf-system-info-type', // class for "type" name
urlLinkClass: 'pf-system-info-url', // class for "url" copy link
// info col/table
systemInfoSectionClass: 'pf-system-info-section', // class for system info section
@@ -166,7 +166,7 @@ define([
fwContestedRow.find('.' + config.systemSovFwPercentageClass).text(percentage);
fwContestedRow.show();
let occupierFactionImage = Util.eveImageUrl('alliance', (occupierFaction ? occupierFaction.id : 0), 64);
let occupierFactionImage = Util.eveImageUrl('factions', (occupierFaction ? occupierFaction.id : 0), 64);
let occupierFactionName = occupierFaction ? occupierFaction.name : '';
fwOccupationRow.find('.' + config.systemSovFwOccupationImageClass)[0].style.setProperty('--bg-image', 'url(\'' + occupierFactionImage + '\')');
@@ -193,20 +193,25 @@ define([
* @param systemData
*/
let getThirdPartySystemLinks = (pages, systemData) => {
let urls = {};
let links = [];
let isWormhole = MapUtil.getSystemTypeInfo(Util.getObjVal(systemData, 'type.id'), 'name') === 'w-space';
let systemName = Util.getObjVal(systemData, 'name') || '';
let regionName = Util.getObjVal(systemData, 'region.name') || '';
let validUrls = 0;
let setDestination = e => {
e.preventDefault();
e.stopPropagation();
Util.setDestination('set_destination', 'system', {id: systemData.systemId, name: systemData.name});
};
for(let i = 0; i < pages.length; i++){
let url = false;
let link = null;
let showInModuleHead = true;
let domain = Util.getObjVal(Init, 'url.' + pages[i]);
if(domain || pages[i] === 'eve'){
if(domain){
// linkOut url
let url = false;
switch(pages[i]){
case 'eve':
url = 'https://client'; // fake url
break;
case 'dotlan':
let systemNameTemp = systemName.replace(/ /g, '_');
let regionNameTemp = regionName.replace(/ /g, '_');
@@ -231,16 +236,41 @@ define([
if(url){
let urlObj = new URL(url);
urls[++validUrls + '_url'] = {
page: pages[i],
domain: urlObj.hostname,
link = {
title: urlObj.hostname,
url: url
};
}
}else{
// custom callback
let action = false;
let title = false;
switch(pages[i]){
case 'eve':
action = setDestination;
title = 'set destination';
showInModuleHead = false;
break;
}
if(action){
link = {
title: title|| pages[i],
action: action
};
}
}
if(link){
links.push(Object.assign({}, link, {
page: pages[i],
showInModuleHead: showInModuleHead
}));
}
}
return urls;
return links;
};
/**
@@ -280,7 +310,7 @@ define([
if(sovDataFact){
sovereigntyPrimary = {
row1Val: 'Faction',
row1Img: Util.eveImageUrl('alliance', sovDataFact.id, 64),
row1Img: Util.eveImageUrl('factions', sovDataFact.id, 64),
row1ImgTitle: sovDataFact.name,
row2Val: sovDataFact.name
};
@@ -288,7 +318,7 @@ define([
if(sovDataAlly){
sovereigntyPrimary = {
row1Val: 'Alliance',
row1Img: Util.eveImageUrl('alliance', sovDataAlly.id, 64),
row1Img: Util.eveImageUrl('alliances', sovDataAlly.id, 64),
row1ImgTitle: sovDataAlly.name,
row2Val: '<' + sovDataAlly.ticker + '>',
row3Label: 'Ally',
@@ -299,7 +329,7 @@ define([
sovereigntySecondary = {
row1Label: 'Corp',
row1Val: sovDataCorp.name,
row1Img: Util.eveImageUrl('corporation', sovDataCorp.id, 64)
row1Img: Util.eveImageUrl('corporations', sovDataCorp.id, 64)
};
}
}
@@ -326,6 +356,7 @@ define([
sovereigntySecondary: sovereigntySecondary ? Object.assign({}, sovereigntyDefault, sovereigntySecondary) : undefined,
static: staticsData,
moduleHeadlineIconClass: config.moduleHeadlineIconClass,
textActionIconCopyClass: config.textActionIconCopyClass,
infoSectionClass: config.systemInfoSectionClass,
descriptionSectionClass: config.descriptionSectionClass,
sovSectionClass: config.systemSovSectionClass,
@@ -378,9 +409,9 @@ define([
systemConstellationLinkClass: config.constellationLinkClass,
systemRegionLinkClass: config.regionLinkClass,
systemTypeLinkClass: config.typeLinkClass,
systemUrlLinkClass: config.urlLinkClass,
systemUrlLinkClass: config.textActionIconCopyClass,
ccpImageServerUrl: Init.url.ccpImageServer,
thirdPartyLinks: getThirdPartySystemLinks(['eve', 'dotlan', 'eveeye', 'anoik'], systemData)
thirdPartyLinks: getThirdPartySystemLinks(['dotlan', 'eveeye', 'anoik', 'eve'], systemData)
};
requirejs(['text!templates/modules/system_info.html', 'mustache', 'summernote.loader'], (template, Mustache, Summernote) => {
@@ -526,7 +557,7 @@ define([
}
// copy system deeplink URL -------------------------------------------------------------------------------
moduleElement.find('.' + config.urlLinkClass).on('click', function(){
moduleElement.find('.' + config.textActionIconCopyClass).on('click', function(){
let mapUrl = $(this).attr('data-url');
Util.copyToClipboard(mapUrl).then(payload => {
if(payload.data){
@@ -562,6 +593,19 @@ define([
return 'Loading...';
};
// 3rd party click callbacks ------------------------------------------------------------------------------
moduleElement.on('click', '[data-link]', e => {
for(let link of data.thirdPartyLinks){
if(
e.target.dataset.link === link.page &&
typeof link.action === 'function'
){
link.action(e);
break;
}
}
});
// init tooltips ------------------------------------------------------------------------------------------
let tooltipElements = moduleElement.find('[data-toggle="tooltip"]');
tooltipElements.tooltip({

View File

@@ -24,9 +24,6 @@ define([
// headline toolbar
moduleHeadlineIconClass: 'pf-module-icon-button', // class for toolbar icons in the head
moduleHeadlineIconAddClass: 'pf-module-icon-button-add', // class for "add structure" icon
moduleHeadlineIconReaderClass: 'pf-module-icon-button-reader', // class for "dScan reader" icon
moduleHeadlineIconRefreshClass: 'pf-module-icon-button-refresh', // class for "refresh" icon
// system intel module
intelTableId: 'pf-intel-table-', // id prefix for all tables in module
@@ -49,7 +46,8 @@ define([
tableCellEllipsisClass: 'pf-table-cell-ellipses-auto', // class for table "ellipsis" cells
tableCellActionClass: 'pf-table-action-cell', // class for "action" cells
tableCellActionIconClass: 'pf-table-action-icon-cell', // class for table "action" icon (icon is part of cell content)
tableCellServicesClass: 'pf-table-services-cell' // class for table station "services" cells
tableCellServicesClass: 'pf-table-services-cell', // class for table station "services" cells
tableCellPopoverClass: 'pf-table-popover-cell' // class for table cells with a "popover"
};
let maxDescriptionLength = 512;
@@ -71,6 +69,15 @@ define([
return '<i class="fas fa-fw fa-id-card ' + config.tableCellActionIconClass + '" title="open ingame" data-toggle="tooltip"></i>';
};
/**
* get a dataTableApi instance from global cache
* @param mapId
* @param systemId
* @param tableType
* @returns {*}
*/
let getDataTableInstance = (mapId, systemId, tableType) => Util.getDataTableInstance(config.intelTableId, mapId, systemId, tableType);
/**
* get dataTable id
* @param mapId
@@ -284,8 +291,9 @@ define([
* @param tableApi
* @param systemId
* @param structureData
* @param bulkData
*/
let showStructureDialog = (moduleElement, tableApi, systemId, structureData) => {
let showStructureDialog = (moduleElement, tableApi, systemId, structureData = null, bulkData = null) => {
let structureStatusData = Util.getObjVal(Init, 'structureStatus');
let statusData = Object.keys(structureStatusData).map((k) => {
@@ -322,6 +330,7 @@ define([
let data = {
id: config.structureDialogId,
structureData: structureData,
bulkData: bulkData,
structureStatus: statusData,
nameInputId: config.nameInputId,
statusSelectId: config.statusSelectId,
@@ -334,9 +343,13 @@ define([
requirejs(['text!templates/dialog/structure.html', 'mustache'], (template, Mustache) => {
let content = Mustache.render(template, data);
let title = 'Structure';
if(bulkData){
title += ' <span class="txt-color txt-color-warning">&nbsp;(' + bulkData.length + ' rows)</span>&nbsp;';
}
let structureDialog = bootbox.dialog({
title: 'Structure',
title: title,
message: content,
show: false,
buttons: {
@@ -346,7 +359,9 @@ define([
},
autoFill: {
label: buttonLabelAutoFill,
className: 'btn-primary' + (disableButtonAutoFill ? ' pf-font-italic disabled' : ''),
className: 'btn-primary' +
(disableButtonAutoFill ? ' pf-font-italic disabled' : '') +
(bulkData ? ' hidden' : ''),
callback: function(){
let form = this.find('form');
form.find('#' + config.nameInputId).val(characterStructureName);
@@ -378,7 +393,20 @@ define([
moduleElement.showLoadingAnimation();
let method = formData.id ? 'PATCH' : 'PUT';
Util.request(method, 'structure', formData.id, formData,
let ids = formData.id;
let data = formData;
if(bulkData){
// bulk update multiple rows
method = 'POST';
ids = [];
data = bulkData.map(structureData => {
structureData.corporationId = formData.corporationId;
return structureData;
});
}
Util.request(method, 'structure', ids, data,
{
moduleElement: moduleElement,
tableApi: tableApi
@@ -524,7 +552,7 @@ define([
if (!last || last.id !== group.id) {
// "stations" are grouped by "raceId" with its "factionId"
// "structures" are grouped by "corporationId" that ADDED it (not the ingame "owner" of it)
let imgType = 'stations' === group.groupedDataKey ? 'alliance' : 'corporation';
let imgType = 'stations' === group.groupedDataKey ? 'factions' : 'corporations';
$(rows).eq(i).before(
'<tr class="group">' +
@@ -573,22 +601,6 @@ define([
$('<h5>', {
class: config.moduleHandlerClass
}),
$('<h5>', {
class: 'pull-right'
}).append(
$('<i>', {
class: ['fas', 'fa-fw', 'fa-plus', config.moduleHeadlineIconClass, config.moduleHeadlineIconAddClass].join(' '),
title: 'add'
}).attr('data-html', 'true').attr('data-toggle', 'tooltip'),
$('<i>', {
class: ['fas', 'fa-fw', 'fa-paste', config.moduleHeadlineIconClass, config.moduleHeadlineIconReaderClass].join(' '),
title: 'D-Scan&nbsp;reader'
}).attr('data-html', 'true').attr('data-toggle', 'tooltip'),
$('<i>', {
class: ['fas', 'fa-fw', 'fa-sync', config.moduleHeadlineIconClass, config.moduleHeadlineIconRefreshClass].join(' '),
title: 'refresh&nbsp;all'
}).attr('data-html', 'true').attr('data-toggle', 'tooltip')
),
$('<h5>', {
text: 'Structures'
})
@@ -608,6 +620,10 @@ define([
},
order: [[10, 'desc' ], [0, 'asc' ]],
rowId: rowData => getRowId('structures', rowData.id),
select: {
style: 'os',
selector: 'td:not(.' + config.tableCellActionClass + ')'
},
language: {
emptyTable: 'No structures recorded',
info: '_START_ to _END_ of _MAX_',
@@ -641,7 +657,7 @@ define([
_: function(data, type, row, meta){
let value = data;
if(type === 'display' && value){
value = '<img src="' + Util.eveImageUrl('type', value, 64) +'"/>';
value = '<img src="' + Util.eveImageUrl('types', value, 64) +'"/>';
}
return value;
}
@@ -675,7 +691,7 @@ define([
let value = data;
if(type === 'display' && value){
value = '<a href="https://zkillboard.com/corporation/' + data + '/" target="_blank" rel="noopener">';
value += '<img src="' + Util.eveImageUrl('corporation', data, 64) + '"/>';
value += '<img src="' + Util.eveImageUrl('corporations', data, 64) + '"/>';
value += '</a>';
}
return value;
@@ -693,7 +709,7 @@ define([
targets: 6,
name: 'note',
title: 'note',
className: [config.tableCellEllipsisClass, 'all'].join(' '),
className: [config.tableCellEllipsisClass, 'all', Util.config.popoverTriggerClass, config.tableCellPopoverClass].join(' '),
data: 'description'
},{
targets: 7,
@@ -726,10 +742,19 @@ define([
$(cell).removeClass(config.tableCellActionClass + ' ' + config.moduleHeadlineIconClass);
}else{
$(cell).on('click', function(e){
// get current row data (important!)
// -> "rowData" param is not current state, values are "on createCell()" state
rowData = tableApi.row( $(cell).parents('tr')).data();
showStructureDialog(moduleElement, tableApi, systemData.systemId, rowData);
let rowData = null;
let bulkData = null;
// check if multiple rows are selected + current row is one of them -> bulk edit
let rowsSelected = tableApi.rows({selected: true});
if(rowsSelected.count() && tableApi.row(rowIndex, {selected: true}).count()){
bulkData = [...new Set(rowsSelected.data().toArray().map(rowData => ({id: rowData.id})))];
}else{
// get current row data (important!)
// -> "rowData" param is not current state, values are "on createCell()" state
rowData = tableApi.row( $(cell).parents('tr')).data();
}
showStructureDialog(moduleElement, tableApi, systemData.systemId, rowData, bulkData);
});
}
}
@@ -743,7 +768,7 @@ define([
data: null,
render: {
display: data => {
let icon = '<i class="fas fa-times txt-color txt-color-redDarker"></i>';
let icon = '<i class="fas fa-times txt-color txt-color-redDark"></i>';
if(data.rowGroupData.id !== corporationId){
icon = '<i class="fas fa-ban txt-color txt-color-grayLight" title="restricted" data-placement="left"></i>';
}
@@ -812,9 +837,9 @@ define([
}
};
$.extend(true, structureDataTableOptions, getDataTableDefaults());
let tableApiStructure = structureTable.DataTable(structureDataTableOptions);
let tableApiStructure = structureTable.DataTable($.extend(true, {}, getDataTableDefaults(), structureDataTableOptions));
// "Responsive" Datatables Plugin
new $.fn.dataTable.Responsive(tableApiStructure);
tableApiStructure.on('responsive-resize', function(e, tableApi, columns){
@@ -822,6 +847,107 @@ define([
tableApi.draw();
});
// "Select" Datatables Plugin
tableApiStructure.select();
// "Buttons" Datatables Plugin
tableApiStructure.on('user-select', function(e, tableApi, type, cell, originalEvent){
let rowData = tableApi.row(cell.index().row).data();
if(Util.getObjVal(rowData, 'rowGroupData.id') !== corporationId){
e.preventDefault();
}
});
let buttons = new $.fn.dataTable.Buttons(tableApiStructure, {
dom: {
container: {
tag: 'h5',
className: 'pull-right'
},
button: {
tag: 'i',
className: ['fas', 'fa-fw', config.moduleHeadlineIconClass].join(' '),
},
buttonLiner: {
tag: null
}
},
name: 'tableTools',
buttons: [
{
name: 'selectToggle',
className: ['fa-check-double'].join(' '),
titleAttr: 'select&nbsp;all',
attr: {
'data-toggle': 'tooltip',
'data-html': true
},
action: function(e, tableApi, node, config){
let indexes = tableApi.rows().eq(0).filter(rowIdx => {
return Util.getObjVal(tableApi.cell(rowIdx, 'rowGroupData:name').data(), 'id') === corporationId;
});
let rowCountAll = tableApi.rows(indexes).count();
let rowCountSelected = tableApi.rows({selected: true}).count();
if(rowCountSelected && (rowCountSelected >= rowCountAll)){
tableApi.rows().deselect();
node.removeClass('active');
}else{
tableApi.rows(indexes).select();
node.addClass('active');
}
}
},
{
name: 'add',
className: 'fa-plus',
titleAttr: 'add',
attr: {
'data-toggle': 'tooltip',
'data-html': true
},
action: function(e, tableApi, node, config){
showStructureDialog(moduleElement, tableApi, systemData.systemId);
}
},
{
name: 'dScan',
className: 'fa-paste',
titleAttr: 'D-Scan&nbsp;reader',
attr: {
'data-toggle': 'tooltip',
'data-html': true
},
action: function(e, tableApi, node, config ){
showDscanReaderDialog(moduleElement, tableApi, systemData);
}
},
{
name: 'refresh',
className: 'fa-sync',
titleAttr: 'refresh&nbsp;all',
attr: {
'data-toggle': 'tooltip',
'data-html': true
},
action: function(e, tableApi, node, config ){
moduleElement.showLoadingAnimation();
Util.request('GET', 'system', systemData.id, {mapId: mapId},
{
moduleElement: moduleElement,
tableApi: tableApi,
removeMissing: true
},
context => context.moduleElement.hideLoadingAnimation()
).then(payload => callbackUpdateTableRows(payload.context, Util.getObjVal(payload.data, 'structures')));
}
}
]
});
tableApiStructure.buttons().container().appendTo(moduleElement.find('.' + config.moduleHeadClass));
if(showStationTable){
// "Stations" table ---------------------------------------------------------------------------------------
@@ -869,7 +995,7 @@ define([
_: function(data, type, row, meta){
let value = data;
if(type === 'display' && value){
value = '<img src="' + Util.eveImageUrl('type', value, 64) +'"/>';
value = '<img src="' + Util.eveImageUrl('types', value, 64) +'"/>';
}
return value;
}
@@ -965,7 +1091,7 @@ define([
let value = data;
if(type === 'display' && value){
value = '<a href="https://zkillboard.com/corporation/' + data + '/" target="_blank" rel="noopener">';
value += '<img src="' + Util.eveImageUrl('corporation', data, 64) + '"/>';
value += '<img src="' + Util.eveImageUrl('corporations', data, 64) + '"/>';
value += '</a>';
}
return value;
@@ -1057,8 +1183,7 @@ define([
}
};
$.extend(true, stationDataTableOptions, getDataTableDefaults());
let tableApiStation = stationTable.DataTable(stationDataTableOptions);
let tableApiStation = stationTable.DataTable($.extend(true, {}, getDataTableDefaults(), stationDataTableOptions));
new $.fn.dataTable.Responsive(tableApiStation);
@@ -1221,30 +1346,6 @@ define([
let structureTableElement = moduleElement.find('.' + config.systemStructuresTableClass);
let tableApi = structureTableElement.DataTable();
// init structure dialog --------------------------------------------------------------------------------------
moduleElement.find('.' + config.moduleHeadlineIconAddClass).on('click', function(e){
showStructureDialog(moduleElement, tableApi, systemData.systemId);
});
// init structure dialog --------------------------------------------------------------------------------------
moduleElement.find('.' + config.moduleHeadlineIconReaderClass).on('click', function(e){
showDscanReaderDialog(moduleElement, tableApi, systemData);
});
// init refresh button ----------------------------------------------------------------------------------------
moduleElement.find('.' + config.moduleHeadlineIconRefreshClass).on('click', function(e){
moduleElement.showLoadingAnimation();
Util.request('GET', 'system', systemData.id, {mapId: mapId},
{
moduleElement: moduleElement,
tableApi: tableApi,
removeMissing: true
},
context => context.moduleElement.hideLoadingAnimation()
).then(payload => callbackUpdateTableRows(payload.context, Util.getObjVal(payload.data, 'structures')));
});
// init listener for global "past" dScan into this page -------------------------------------------------------
moduleElement.on('pf:updateIntelModuleByClipboard', function(e, clipboard){
updateStructureTableByClipboard(systemData, clipboard, {
@@ -1252,6 +1353,38 @@ define([
tableApi: tableApi
});
});
// init popovers for some table cells -------------------------------------------------------------------------
moduleElement.hoverIntent({
over: function(e){
let tableApi = getDataTableInstance(mapId, systemData.id, 'structure');
// simple <table> for layout (CSS)
let cellData = tableApi.cell(this).data();
if(cellData && cellData.length){
let content = '<table><tr><td>' + cellData.replace(/\r?\n/g, '<br />') + '</td></tr></table>';
let options = {
placement: 'top',
html: true,
trigger: 'manual',
container: 'body',
title: '',
content: content,
delay: {
show: 0,
hide: 0
},
};
$(this).popover(options).popover('show');
}
},
out: function(e){
$(this).destroyPopover();
},
selector: '.' + config.tableCellPopoverClass
});
};
/**
@@ -1263,8 +1396,8 @@ define([
let stationTable = moduleElement.find('.' + config.systemStationsTableClass);
let tableApiStructure = structureTable.DataTable();
let tableApiStation = stationTable.DataTable();
tableApiStructure.destroy();
tableApiStation.destroy();
tableApiStructure.destroy(true);
tableApiStation.destroy(true);
};
return {

View File

@@ -6,8 +6,8 @@ define([
'jquery',
'app/init',
'app/util',
'morris'
], ($, Init, Util, Morris) => {
'app/lib/cache'
], ($, Init, Util, Cache) => {
'use strict';
let config = {
@@ -39,7 +39,12 @@ define([
maxCountKills: 43
};
let cache = {};
let cache = new Cache({
name: 'killboardModule',
ttl: 60 * 60,
maxSize: 600,
debug: false
});
/**
*
@@ -59,9 +64,10 @@ define([
*/
let loadKillmailData = (requestData, context, callback) => {
let cacheKey = 'killmail_' + requestData.killId;
if(cache[cacheKey]){
let responseData = cache.get(cacheKey);
if(responseData){
// ... already cached -> return from cache
callback(context, cache[cacheKey])
callback(context, responseData)
.then(payload => showKills(payload.data.killboardElement, payload.data.systemId, payload.data.chunkSize));
}else{
// ...not cached -> request data
@@ -73,7 +79,7 @@ define([
dataType: 'json',
context: context
}).done(function(responseData){
cache[cacheKey] = responseData;
cache.set(cacheKey, responseData);
callback(this, responseData)
.then(payload => showKills(payload.data.killboardElement, payload.data.systemId, payload.data.chunkSize));
@@ -93,12 +99,14 @@ define([
*/
let showKills = (killboardElement, systemId, chunkSize) => {
if(chunkSize){
let cacheKey = 'zkb_' + systemId;
let data = cache.getOrDefault(cacheKey, []);
if(
killboardElement.children().length < config.maxCountKills &&
cache['zkb_' + systemId].length
data.length
){
// next killmail to load
let nextZkb = cache['zkb_' + systemId].shift();
let nextZkb = data.shift();
loadKillmailData({
killId: parseInt(nextZkb.killmail_id) || 0,
@@ -219,7 +227,7 @@ define([
}).done(function(result){
// zkb result needs to be cached and becomes reduced on "load more"
let cacheKey = 'zkb_' + systemData.systemId;
cache[cacheKey] = result;
cache.set(cacheKey, result);
if(result.length){
// kills found -> insert hidden warning for recent kills

View File

@@ -761,7 +761,7 @@ define([
let flagButton = '<i class="fas ' + ['fa-shield-alt', 'txt-color', flagButtonClass].join(' ') + '"></i>';
let reloadButton = '<i class="fas ' + ['fa-sync'].join(' ') + '"></i>';
let searchButton = '<i class="fas ' + ['fa-search'].join(' ') + '"></i>';
let deleteButton = '<i class="fas ' + ['fa-times', 'txt-color', 'txt-color-redDarker'].join(' ') + '"></i>';
let deleteButton = '<i class="fas ' + ['fa-times', 'txt-color', 'txt-color-redDark'].join(' ') + '"></i>';
// default row data (e.g. no route found)
let tableRowData = {
@@ -1240,10 +1240,10 @@ define([
popoverRoot.data('bs.popover').tip().find('a').on('click', function(){
// hint: "data" attributes should be in lower case!
let systemData = {
systemId: $(this).data('systemid'),
id: $(this).data('systemid'),
name: $(this).data('name')
};
Util.setDestination(systemData, 'set_destination');
Util.setDestination('set_destination', 'system', systemData);
// close popover
popoverRoot.popover('hide');

View File

@@ -10,8 +10,9 @@ define([
'app/counter',
'app/map/map',
'app/map/util',
'app/lib/cache',
'app/ui/form_element'
], ($, Init, Util, bootbox, Counter, Map, MapUtil, FormElement) => {
], ($, Init, Util, bootbox, Counter, Map, MapUtil, Cache, FormElement) => {
'use strict';
let config = {
@@ -51,6 +52,7 @@ define([
sigTableEditSigNameInput: 'pf-sig-table-edit-name-input', // class for editable fields (sig name)
tableCellTypeClass: 'pf-table-type-cell', // class for "type" cells
tableCellConnectionClass: 'pf-table-connection-cell', // class for "connection" cells
tableCellFocusClass: 'pf-table-focus-cell', // class for "tab-able" cells. enable focus()
tableCellCounterClass: 'pf-table-counter-cell', // class for "counter" cells
@@ -76,7 +78,12 @@ define([
sigInfoCountConDeleteId: 'pf-sig-info-count-con-delete' // id for "connection delete" counter
};
let sigNameCache = {}; // cache signature names
let sigTypeOptionsCache = new Cache({ // cache signature names
name: 'sigTypeOptions',
ttl: 60 * 5,
maxSize: 100,
debug: false
});
let validSignatureNames = [ // allowed signature type/names
'Cosmic Anomaly',
@@ -135,7 +142,7 @@ define([
reason = jqXHR.statusText;
}else if(jqXHR.name){
// validation error new sig (new row data save function)
reason = jqXHR.name;
reason = jqXHR.name.msg;
// re-open "name" fields (its a collection of fields but we need "id" field)
jqXHR.name.field.$element.editable('show');
}else{
@@ -221,6 +228,36 @@ define([
return sum;
};
/**
* Some signatures types can spawn in more than one 'areaId' for a 'groupId'
* -> e.g. a 'shattered' C3 WHs have Combat/Relic/.. sites from C2, C3, c4!
* https://github.com/exodus4d/pathfinder/issues/875
* @param systemTypeId
* @param areaId
* @param groupId
* @param shattered
* @returns {[*]}
*/
let getAreaIdsForSignatureTypeOptions = (systemTypeId, areaId, groupId, shattered = false) => {
let areaIds = [areaId];
if(
systemTypeId === 1 && shattered &&
[1, 2, 3, 4, 5, 6].includes(areaId) &&
[1, 2, 3].includes(groupId) // Combat, Relic, Data
){
areaIds = [areaId - 1, areaId, areaId + 1].filter(areaId => areaId >= 1 && areaId <= 6);
}else if(
systemTypeId === 1 && shattered &&
[1, 2, 3, 4, 5, 6].includes(areaId) &&
[4, 6].includes(groupId) // Gas, Ore
){
areaIds = [1, 2, 3, 4, 5, 6, 13];
}
return areaIds;
};
/**
* get possible frig holes that could spawn in a system
* filtered by "systemTypeId"
@@ -238,13 +275,14 @@ define([
/**
* get all signature types that can exist for a given system
* -> result is partially cached
* @param systemData
* @param systemTypeId
* @param areaId
* @param groupId
* @returns {Array}
* @param statics
* @param shattered
* @returns {[]|*}
*/
let getAllSignatureNames = (systemData, systemTypeId, areaId, groupId) => {
let getSignatureTypeOptions = (systemTypeId, areaId, groupId, {statics = null, shattered = false} = {}) => {
systemTypeId = parseInt(systemTypeId || 0);
areaId = parseInt(areaId || 0);
groupId = parseInt(groupId || 0);
@@ -255,18 +293,23 @@ define([
return newSelectOptions;
}
let cacheKey = [systemTypeId, areaId, groupId].join('_');
// check if sig types require more than one 'areaId' to be checked
let areaIds = getAreaIdsForSignatureTypeOptions(systemTypeId, areaId, groupId, shattered);
let cacheKey = [systemTypeId, ...areaIds, groupId].join('_');
newSelectOptions = sigTypeOptionsCache.getOrDefault(cacheKey, []);
// check for cached signature names
if(sigNameCache.hasOwnProperty( cacheKey )){
if(newSelectOptions.length){
// cached signatures do not include static WHs!
// -> ".slice(0)" creates copy
newSelectOptions = sigNameCache[cacheKey].slice(0);
newSelectOptions = newSelectOptions.slice(0);
newSelectOptionsCount = getOptionsCount('children', newSelectOptions);
}else{
// get new Options ----------
// get all possible "static" signature names by the selected groupId
let tempSelectOptions = Util.getAllSignatureNames(systemTypeId, areaId, groupId);
let tempSelectOptions = Util.getSignatureTypeNames(systemTypeId, areaIds, groupId);
// format options into array with objects advantages: keep order, add more options (whs), use optgroup
if(tempSelectOptions){
@@ -303,7 +346,7 @@ define([
frigateHoles.hasOwnProperty(frigKey)
){
newSelectOptionsCount++;
frigateWHData.push( {value: newSelectOptionsCount, text: frigateHoles[frigKey]} );
frigateWHData.push({value: newSelectOptionsCount, text: frigateHoles[frigKey]});
}
}
@@ -320,7 +363,7 @@ define([
Init.drifterWormholes.hasOwnProperty(drifterKey)
){
newSelectOptionsCount++;
drifterWHData.push( {value: newSelectOptionsCount, text: Init.drifterWormholes[drifterKey]} );
drifterWHData.push({value: newSelectOptionsCount, text: Init.drifterWormholes[drifterKey]});
}
}
@@ -337,12 +380,12 @@ define([
Init.incomingWormholes.hasOwnProperty(incomingKey)
){
newSelectOptionsCount++;
incomingWHData.push( {value: newSelectOptionsCount, text: Init.incomingWormholes[incomingKey]} );
incomingWHData.push({value: newSelectOptionsCount, text: Init.incomingWormholes[incomingKey]});
}
}
if(incomingWHData.length > 0){
newSelectOptions.push({ text: 'Incoming', children: incomingWHData});
newSelectOptions.push({text: 'Incoming', children: incomingWHData});
}
}else{
// groups without "children" (optgroup) should be sorted by "value"
@@ -351,20 +394,20 @@ define([
}
// update cache (clone array) -> further manipulation to this array, should not be cached
sigNameCache[cacheKey] = newSelectOptions.slice(0);
sigTypeOptionsCache.set(cacheKey, newSelectOptions.slice(0));
}
// static wormholes (DO NOT CACHE) (not all C2 WHs have the same statics,...
// static wormholes (DO NOT CACHE) (not all C2 WHs have the same statics..)
if(groupId === 5){
// add static WH(s) for this system
if(systemData.statics){
if(statics){
let staticWHData = [];
for(let wormholeName of systemData.statics){
for(let wormholeName of statics){
let wormholeData = Object.assign({}, Init.wormholes[wormholeName]);
let staticWHName = wormholeData.name + ' - ' + wormholeData.security;
newSelectOptionsCount++;
staticWHData.push( {value: newSelectOptionsCount, text: staticWHName} );
staticWHData.push({value: newSelectOptionsCount, text: staticWHName});
}
if(staticWHData.length > 0){
@@ -382,11 +425,11 @@ define([
* @param groupId
* @returns {Array}
*/
let getAllSignatureNamesBySystem = (systemElement, groupId) => {
let getSignatureTypeOptionsBySystem = (systemElement, groupId) => {
let systemTypeId = systemElement.data('typeId');
let areaId = Util.getAreaIdBySecurity(systemElement.data('security'));
let systemData = {statics: systemElement.data('statics')};
return getAllSignatureNames(systemData, systemTypeId, areaId, groupId);
return getSignatureTypeOptions(systemTypeId, areaId, groupId, systemData);
};
/**
@@ -473,7 +516,7 @@ define([
// hide row
// stop sig counter by adding a stopClass to each <td>, remove padding
cellElements.addClass('stopCounter')
cellElements.addClass(Counter.config.counterStopClass)
.velocity({
paddingTop: [0, '4px'],
paddingBottom: [0, '4px'],
@@ -882,11 +925,11 @@ define([
// try to get "typeId" from description string
let sigDescriptionLowerCase = sigDescription.toLowerCase();
let typeOptions = getAllSignatureNames(
systemData,
let typeOptions = getSignatureTypeOptions(
systemData.type.id,
Util.getAreaIdBySecurity(systemData.security),
sigGroupId
sigGroupId,
systemData
);
for(let [key, name] of Object.entries(Util.flattenXEditableSelectArray(typeOptions))){
@@ -1196,12 +1239,16 @@ define([
*/
let activateCell = (cell) => {
let cellElement = cell.nodes().to$();
// NO xEditable
cellElement.focus();
// check if cell is visible and not e.g. immediately filtered out by a search filter
// -> https://github.com/exodus4d/pathfinder/issues/865
if(cellElement.is(':visible')){
// NO xEditable
cellElement.focus();
if( cellElement.data('editable') ){
// cell is xEditable field -> show xEditable form
cellElement.editable('show');
if( cellElement.data('editable') ){
// cell is xEditable field -> show xEditable form
cellElement.editable('show');
}
}
};
@@ -1594,11 +1641,9 @@ define([
},
validate: function(value){
let msg = false;
let mbLength = [...$.trim(value)].length; // unicode beware
if(mbLength < 3){
msg = 'Id is less than min of "3"';
}else if(mbLength > 10){
msg = 'Id is more than max of "10"';
//let mbLength = [...$.trim(value)].length; // unicode beware
if(! value.trimChars().match(/^[a-zA-Z]{3}-\d{3}$/)){
msg = 'ID format invalid. E.g.: ABC-123';
}
if(msg){
@@ -1717,7 +1762,7 @@ define([
title: 'type',
type: 'string', // required for sort/filter because initial data type is numeric
width: 180,
class: [config.tableCellFocusClass].join(' '),
class: [config.tableCellFocusClass, config.tableCellTypeClass].join(' '),
data: 'typeId',
createdCell: function(cell, cellData, rowData, rowIndex, colIndex){
let tableApi = this.api();
@@ -1745,18 +1790,18 @@ define([
// -> "rowData" param is not current state, values are "on createCell()" state
let rowData = tableApi.row($(cell).parents('tr')).data();
let typeOptions = getAllSignatureNames(
systemData,
let typeOptions = getSignatureTypeOptions(
systemData.type.id,
Util.getAreaIdBySecurity(systemData.security),
rowData.groupId
rowData.groupId,
systemData
);
return typeOptions;
},
display: function(value, sourceData){
let selected = $.fn.editableutils.itemsByValue(value, sourceData);
if(selected.length && selected[0].value > 0){
$(this).html(FormElement.formatSignatureTypeSelectionData({text: selected[0].text}));
$(this).html(FormElement.formatSignatureTypeSelectionData({text: selected[0].text}, undefined, {showWhSizeLabel: true}));
}else{
$(this).empty();
}
@@ -1917,16 +1962,7 @@ define([
width: 80,
className: ['text-right', config.tableCellCounterClass, 'min-screen-d'].join(' '),
data: 'updated.updated',
defaultContent: '',
createdCell: function(cell, cellData, rowData, rowIndex, colIndex){
// highlight cell
let diff = Math.floor((new Date()).getTime()) - cellData * 1000;
// age > 1 day
if(diff > 86400000){
$(cell).addClass('txt-color txt-color-warning');
}
}
defaultContent: ''
},{
targets: 8,
name: 'info',
@@ -1957,7 +1993,7 @@ define([
display: (cellData, type, rowData, meta) => {
let val = '<i class="fas fa-plus"></i>';
if(rowData.id){
val = '<i class="fas fa-times txt-color txt-color-redDarker"></i>';
val = '<i class="fas fa-times txt-color txt-color-redDark"></i>';
}
return val;
}
@@ -2096,6 +2132,20 @@ define([
$(this).trigger('click');
}
});
},
rowCallback: function(){
let tableApi = this.api();
let time = Math.floor((new Date()).getTime());
tableApi.cells(null, ['updated:name']).every(function(rowIndex, colIndex, tableLoopCount, cellLoopCount){
let cell = this;
let node = cell.node();
let cellData = cell.data();
let diff = time - cellData * 1000;
// highlight cell: age > 1 day
$(node).toggleClass('txt-color txt-color-warning', diff > 86400000);
});
}
};
@@ -2359,7 +2409,7 @@ define([
let getIconByAction = action => {
switch(action){
case 'add': return 'fa-plus txt-color-green';
case 'delete': return 'fa-times txt-color-redDarker';
case 'delete': return 'fa-times txt-color-redDark';
case 'edit': return 'fa-pen txt-color-orangeDark';
case 'undo': return 'fa-undo txt-color-grayLight';
case 'sync': return 'fa-exchange-alt txt-color-orangeDark';
@@ -2600,6 +2650,7 @@ define([
keyNavigation(tableApi, e);
});
Counter.initTableCounter(this, ['created:name', 'updated:name']);
}
};
@@ -3303,6 +3354,6 @@ define([
updateModule: updateModule,
beforeHide: beforeHide,
beforeDestroy: beforeDestroy,
getAllSignatureNamesBySystem: getAllSignatureNamesBySystem
getSignatureTypeOptionsBySystem: getSignatureTypeOptionsBySystem
};
});

View File

@@ -4,7 +4,8 @@
define([
'jquery',
'app/init',
'app/console',
'app/lib/prototypes',
'app/lib/console',
'conf/system_effect',
'conf/signature_type',
'bootbox',
@@ -19,7 +20,7 @@ define([
'bootstrapConfirmation',
'bootstrapToggle',
'select2'
], ($, Init, Con, SystemEffect, SignatureType, bootbox, localforage) => {
], ($, Init, Proto, Con, SystemEffect, SignatureType, bootbox, localforage) => {
'use strict';
@@ -448,7 +449,7 @@ define([
* @param recursive
* @returns {*}
*/
$.fn.destroyTooltip = function(recursive){
$.fn.destroyTooltips = function(recursive){
return this.each(function(){
let element = $(this);
let tooltipSelector = '[title]';
@@ -554,7 +555,7 @@ define([
userData: userData,
otherCharacters: () => {
return userData.characters.filter((character, i) => {
let characterImage = eveImageUrl('character', character.id);
let characterImage = eveImageUrl('characters', character.id);
// preload image (prevent UI flicker
let img= new Image();
img.src = characterImage;
@@ -742,7 +743,7 @@ define([
let defaultOptions = {
dismissible: true,
messageId: 'pf-alert-' + Math.random().toString(36).substring(7),
messageId: getRandomString('pf-alert-'),
messageTypeClass: messageTypeClass,
messageTextClass: messageTextClass,
insertElement: 'replace'
@@ -856,17 +857,39 @@ define([
/**
* get CCP image URLs for
* @param type 'alliance'|'corporation'|'character'|'type'|'render'
* @param id
* @param resourceType 'alliances'|'corporations'|'characters'|'types'
* @param $resourceId
* @param size
* @param resourceVariant
* @returns {boolean}
*/
let eveImageUrl = (type, id, size = 32) => {
let eveImageUrl = (resourceType, $resourceId, size = 32, resourceVariant = undefined) => {
let url = false;
if(typeof type === 'string' && typeof id === 'number' && typeof size === 'number'){
type = type.capitalize();
let format = type === 'Character' ? 'jpg' : 'png';
url = Init.url.ccpImageServer + '/' + type + '/' + id + '_' + size + '.' + format;
if(
typeof resourceType === 'string' &&
typeof $resourceId === 'number' &&
typeof size === 'number'
){
resourceType = resourceType.toLowerCase();
if(!resourceVariant){
switch(resourceType){
// faction icons are on 'corporations' endpoint.. CCP fail?!
case 'factions': resourceType = 'corporations'; // jshint ignore:line
case 'alliances':
case 'corporations': resourceVariant = 'logo'; break;
case 'characters': resourceVariant = 'portrait'; break;
case 'types': resourceVariant = 'icon'; break;
default:
console.warn('Invalid resourceType: %o for in eveImageUrl()', resourceType);
}
}
url = [Init.url.ccpImageServer, resourceType, $resourceId, resourceVariant].join('/');
let params = {size: size};
let searchParams = new URLSearchParams(params); // jshint ignore:line
url += '?' + searchParams.toString();
}
return url;
};
@@ -951,85 +974,6 @@ define([
*/
let initPrototypes = () => {
/**
* Array diff
* [1,2,3,4,5].diff([4,5,6]) => [1,2,3]
* @param a
* @returns {*[]}
*/
Array.prototype.diff = function(a){
return this.filter(i => !a.includes(i));
};
/**
* Array intersect
* [1,2,3,4,5].intersect([4,5,6]) => [4,5]
* @param a
* @returns {*[]}
*/
Array.prototype.intersect = function(a){
return this.filter(i => a.includes(i));
};
/**
* compares two arrays if all elements in a are also in b
* element order is ignored
* @param a
* @returns {boolean}
*/
Array.prototype.equalValues = function(a){
return this.diff(a).concat(a.diff(this)).length === 0;
};
/**
* sort array of objects by property name
* @param p
* @returns {Array.<T>}
*/
Array.prototype.sortBy = function(p){
return this.slice(0).sort((a,b) => {
return (a[p] > b[p]) ? 1 : (a[p] < b[p]) ? -1 : 0;
});
};
/**
* capitalize first letter
* @returns {string}
*/
String.prototype.capitalize = function(){
return this.charAt(0).toUpperCase() + this.slice(1);
};
/**
* get hash from string
* @returns {number}
*/
String.prototype.hashCode = function(){
let hash = 0, i, chr;
if(this.length === 0) return hash;
for(i = 0; i < this.length; i++){
chr = this.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
};
String.prototype.trimLeftChars = function(charList){
if(charList === undefined)
charList = '\\s';
return this.replace(new RegExp('^[' + charList + ']+'), '');
};
String.prototype.trimRightChars = function(charList){
if(charList === undefined)
charList = '\\s';
return this.replace(new RegExp('[' + charList + ']+$'), '');
};
String.prototype.trimChars = function(charList){
return this.trimLeftChars(charList).trimRightChars(charList);
};
initPassiveEvents();
};
@@ -1559,6 +1503,14 @@ define([
return Init.timer[updateKey].CURRENT_DELAY;
};
/**
* get a random string
* -> e.g. as for Ids
* @param prefix
* @returns {string}
*/
let getRandomString = (prefix = 'id_') => prefix + Math.random().toString(36).substring(2,10);
/**
* get date obj with current EVE Server Time.
* @returns {Date}
@@ -1622,6 +1574,21 @@ define([
return parts;
};
/**
* format json object with "time parts" into string
* @param parts
* @returns {string}
*/
let formatTimeParts = parts => {
let label = '';
if(parts.days){
label += parts.days + 'd ';
}
label += ('00' + parts.hours).slice(-2);
label += ':' + ('00' + parts.min).slice(-2);
return label;
};
/**
* start time measurement by a unique string identifier
* @param timerName
@@ -1669,23 +1636,25 @@ define([
* @param maxCharLength
*/
let updateCounter = (field, charCounterElement, maxCharLength) => {
let value = field.val();
let inputLength = value.length;
if(field.length){
let value = field.val();
let inputLength = value.length;
// line breaks are 2 characters!
let newLines = value.match(/(\r\n|\n|\r)/g);
let addition = 0;
if(newLines != null){
addition = newLines.length;
}
inputLength += addition;
// line breaks are 2 characters!
let newLines = value.match(/(\r\n|\n|\r)/g);
let addition = 0;
if(newLines != null){
addition = newLines.length;
}
inputLength += addition;
charCounterElement.text(maxCharLength - inputLength);
charCounterElement.text(maxCharLength - inputLength);
if(maxCharLength <= inputLength){
charCounterElement.toggleClass('txt-color-red', true);
}else{
charCounterElement.toggleClass('txt-color-red', false);
if(maxCharLength <= inputLength){
charCounterElement.toggleClass('txt-color-red', true);
}else{
charCounterElement.toggleClass('txt-color-red', false);
}
}
};
@@ -1819,7 +1788,7 @@ define([
let key = 'tabId';
let tabId = sessionStorage.getItem(key);
if(tabId === null){
tabId = Math.random().toString(36).substr(2, 5);
tabId = getRandomString();
sessionStorage.setItem(key, tabId);
}
return tabId;
@@ -2543,23 +2512,16 @@ define([
};
/**
* get Signature names out of global
* @param systemTypeId
* @param areaId
* @param sigGroupId
* get signature 'type' options for a systemTypeId
* -> areaIds is array! This is used for "Shattered WHs" where e.g.:
* Combat/Relic/.. sites from multiple areaIds (C1, C2, C3) can spawn in a C2,...
* @param systemTypeId 1 == w-space; 2 == k-space; 3 == a-space
* @param areaIds 1 == c1; 2 == c2; 12 == Thera; 13 == Shattered Frig;...
* @param sigGroupId 1 == Combat; 2 == Relic; 3 == Data; ...
* @returns {{}}
*/
let getAllSignatureNames = (systemTypeId, areaId, sigGroupId) => {
let signatureNames = {};
if(
SignatureType[systemTypeId] &&
SignatureType[systemTypeId][areaId] &&
SignatureType[systemTypeId][areaId][sigGroupId]
){
signatureNames = SignatureType[systemTypeId][areaId][sigGroupId];
}
return signatureNames;
let getSignatureTypeNames = (systemTypeId, areaIds, sigGroupId) => {
return objCombine(...areaIds.map(areaId => getObjVal(SignatureType, [systemTypeId, areaId, sigGroupId].join('.')) || {}));
};
/**
@@ -3349,6 +3311,21 @@ define([
return obj;
}, {});
/**
* combines multiple objects into one object
* -> removes duplicate values
* -> properties are indexed 1, 2,..n
* @param objects
* @returns {{[p: string]: *}}
*/
let objCombine = (...objects) => {
let combined = objects.reduce((acc, obj) => acc.concatFilter(Object.values(obj)), []);
combined.unshift(''); // properties should start at 1 (not 0)
combined = Object.assign({}, combined);
delete combined[0];
return combined;
};
/**
* get deep json object value if exists
* -> e.g. key = 'first.last.third' string
@@ -3483,9 +3460,11 @@ define([
initDefaultSelect2Config: initDefaultSelect2Config,
initDefaultEditableConfig: initDefaultEditableConfig,
getCurrentTriggerDelay: getCurrentTriggerDelay,
getRandomString: getRandomString,
getServerTime: getServerTime,
convertTimestampToServerTime: convertTimestampToServerTime,
getTimeDiffParts: getTimeDiffParts,
formatTimeParts: formatTimeParts,
timeStart: timeStart,
timeStop: timeStop,
updateCounter: updateCounter,
@@ -3515,7 +3494,7 @@ define([
getTrueSecClassForSystem: getTrueSecClassForSystem,
getStatusInfoForSystem: getStatusInfoForSystem,
getSignatureGroupOptions: getSignatureGroupOptions,
getAllSignatureNames: getAllSignatureNames,
getSignatureTypeNames: getSignatureTypeNames,
getAreaIdBySecurity: getAreaIdBySecurity,
setCurrentMapUserData: setCurrentMapUserData,
getCurrentMapUserData: getCurrentMapUserData,

View File

@@ -1,256 +0,0 @@
/*!
* Bootstrap Confirmation v1.0.7
* https://github.com/tavicu/bs-confirmation
*/
+function ($) {
'use strict';
//var for check event at body can have only one.
var event_body = false;
// CONFIRMATION PUBLIC CLASS DEFINITION
// ===============================
var Confirmation = function (element, options) {
var that = this;
this.init('confirmation', element, options);
if (options.selector) {
$(element).on('click.bs.confirmation', options.selector, function(e) {
e.preventDefault();
});
} else {
$(element).on('show.bs.confirmation', function(event) {
that.runCallback(that.options.onShow, event, that.$element);
that.$element.addClass('open');
if (that.options.singleton) {
$(that.options.all_selector).not(that.$element).each(function() {
if ($(this).hasClass('open')) {
$(this).confirmation('hide');
}
});
}
}).on('hide.bs.confirmation', function(event) {
that.runCallback(that.options.onHide, event, that.$element);
that.$element.removeClass('open');
}).on('shown.bs.confirmation', function(e) {
if (!that.isPopout() && !event_body) {
return;
}
event_body = $('body').on('click', function (e) {
if (that.$element.is(e.target)) return;
if (that.$element.has(e.target).length) return;
if ($('.popover').has(e.target).length) return;
that.hide();
that.inState.click = false;
$('body').unbind(e);
event_body = false;
return;
});
}).on('click.bs.confirmation', function(e) {
e.preventDefault();
});
}
}
if (!$.fn.popover || !$.fn.tooltip) throw new Error('Confirmation requires popover.js and tooltip.js');
Confirmation.VERSION = '1.0.7'
Confirmation.DEFAULTS = $.extend({}, $.fn.popover.Constructor.DEFAULTS, {
placement : 'right',
title : 'Are you sure?',
btnOkClass : 'btn btn-sm btn-danger',
btnOkLabel : 'Delete',
btnOkIcon : 'glyphicon glyphicon-ok',
btnCancelClass : 'btn btn-sm btn-default',
btnCancelLabel : 'Cancel',
btnCancelIcon : 'glyphicon glyphicon-remove',
href : '#',
target : '_self',
singleton : true,
popout : true,
onShow : function(event, element) {},
onHide : function(event, element) {},
onConfirm : function(event, element) {},
onCancel : function(event, element) {},
template : '<div class="popover"><div class="arrow"></div>'
+ '<h3 class="popover-title"></h3>'
+ '<div class="popover-content">'
+ ' <a data-apply="confirmation">Yes</a>'
+ ' <a data-dismiss="confirmation">No</a>'
+ '</div>'
+ '</div>'
});
// NOTE: CONFIRMATION EXTENDS popover.js
// ================================
Confirmation.prototype = $.extend({}, $.fn.popover.Constructor.prototype);
Confirmation.prototype.constructor = Confirmation;
Confirmation.prototype.getDefaults = function () {
return Confirmation.DEFAULTS;
}
Confirmation.prototype.setContent = function () {
var that = this;
var $tip = this.tip();
var title = this.getTitle();
var $btnOk = $tip.find('[data-apply="confirmation"]');
var $btnCancel = $tip.find('[data-dismiss="confirmation"]');
var options = this.options
$btnOk.addClass(this.getBtnOkClass())
.html(this.getBtnOkLabel())
.prepend($('<i></i>').addClass(this.getBtnOkIcon()), " ")
.attr('href', this.getHref())
.attr('target', this.getTarget())
.off('click').on('click', function(event) {
that.runCallback(that.options.onConfirm, event, that.$element);
// If the button is a submit one
if (that.$element.attr('type') == 'submit') {
var form = that.$element.closest('form');
var novalidate = form.attr('novalidate') !== undefined;
if (novalidate || form[0].checkValidity()) {
form.submit();
}
}
that.hide();
that.inState.click = false;
that.$element.trigger($.Event('confirm.bs.confirmation'));
});
$btnCancel.addClass(this.getBtnCancelClass())
.html(this.getBtnCancelLabel())
.prepend($('<i></i>').addClass(this.getBtnCancelIcon()), " ")
.off('click').on('click', function(event) {
that.runCallback(that.options.onCancel, event, that.$element);
that.hide();
that.inState.click = false;
that.$element.trigger($.Event('cancel.bs.confirmation'));
});
$tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title);
$tip.removeClass('fade top bottom left right in');
// IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do
// this manually by checking the contents.
if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide();
}
Confirmation.prototype.getBtnOkClass = function () {
return this.$element.data('btnOkClass') ||
(typeof this.options.btnOkClass == 'function' ? this.options.btnOkClass.call(this, this.$element) : this.options.btnOkClass);
}
Confirmation.prototype.getBtnOkLabel = function () {
return this.$element.data('btnOkLabel') ||
(typeof this.options.btnOkLabel == 'function' ? this.options.btnOkLabel.call(this, this.$element) : this.options.btnOkLabel);
}
Confirmation.prototype.getBtnOkIcon = function () {
return this.$element.data('btnOkIcon') ||
(typeof this.options.btnOkIcon == 'function' ? this.options.btnOkIcon.call(this, this.$element) : this.options.btnOkIcon);
}
Confirmation.prototype.getBtnCancelClass = function () {
return this.$element.data('btnCancelClass') ||
(typeof this.options.btnCancelClass == 'function' ? this.options.btnCancelClass.call(this, this.$element) : this.options.btnCancelClass);
}
Confirmation.prototype.getBtnCancelLabel = function () {
return this.$element.data('btnCancelLabel') ||
(typeof this.options.btnCancelLabel == 'function' ? this.options.btnCancelLabel.call(this, this.$element) : this.options.btnCancelLabel);
}
Confirmation.prototype.getBtnCancelIcon = function () {
return this.$element.data('btnCancelIcon') ||
(typeof this.options.btnCancelIcon == 'function' ? this.options.btnCancelIcon.call(this, this.$element) : this.options.btnCancelIcon);
}
Confirmation.prototype.getTitle = function () {
return this.$element.data('confirmation-title') ||
this.$element.data('title') ||
this.$element.attr('title') ||
(typeof this.options.title == 'function' ? this.options.title.call(this, this.$element) : this.options.title);
}
Confirmation.prototype.getHref = function () {
return this.$element.data('href') ||
this.$element.attr('href') ||
(typeof this.options.href == 'function' ? this.options.href.call(this, this.$element) : this.options.href);
}
Confirmation.prototype.getTarget = function () {
return this.$element.data('target') ||
this.$element.attr('target') ||
(typeof this.options.target == 'function' ? this.options.target.call(this, this.$element) : this.options.target);
}
Confirmation.prototype.isPopout = function () {
var popout = this.$element.data('popout') ||
(typeof this.options.popout == 'function' ? this.options.popout.call(this, this.$element) : this.options.popout);
if (popout == 'false') popout = false;
return popout
}
Confirmation.prototype.runCallback = function (callback, event, element) {
if (typeof callback == 'function') {
callback.call(this, event, element);
} else if (typeof callback == 'string') {
eval(callback);
}
}
// CONFIRMATION PLUGIN DEFINITION
// =========================
var old = $.fn.confirmation;
$.fn.confirmation = function (option) {
var that = this;
return this.each(function () {
var $this = $(this);
var data = $this.data('bs.confirmation');
var options = typeof option == 'object' && option;
options = options || {};
options.all_selector = that.selector;
if (!data && option == 'destroy') return;
if (!data) $this.data('bs.confirmation', (data = new Confirmation(this, options)));
if (typeof option == 'string') data[option]();
});
}
$.fn.confirmation.Constructor = Confirmation
// CONFIRMATION NO CONFLICT
// ===================
$.fn.confirmation.noConflict = function () {
$.fn.confirmation = old;
return this;
}
}(jQuery);

5
js/lib/bootstrap-confirmation.min.js vendored Normal file

File diff suppressed because one or more lines are too long

7
js/lib/easytimer.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,358 +0,0 @@
/*
@title:
Drag to Select
@version:
1.1
@author:
Andreas Lagerkvist
@date:
2009-04-06
@url:
http://andreaslagerkvist.com/jquery/drag-to-select/
@license:
http://creativecommons.org/licenses/by/3.0/
@copyright:
2008 Andreas Lagerkvist (andreaslagerkvist.com)
@requires:
jquery, jquery.dragToSelect.css
@does:
Use this plug-in to allow your users to select certain elements by dragging a "select box". Works very similar to how you can drag-n-select files and folders in most OS:es.
@howto:
$('#my-files').dragToSelect(selectables: 'li'); would make every li in the #my-files-element selectable by dragging. The li:s will recieve a "selected"-class when they are within range of the select box when user drops.
Make sure a parent-element of the selectables has position: relative as well as overflow: auto or scroll.
@exampleHTML:
<ul>
<li><img src="http://exscale.se/__files/3d/lamp-and-mates/lamp-and-mates-01_small.jpg" alt="Lamp and Mates" /></li>
<li><img src="http://exscale.se/__files/3d/stugan-winter_small.jpg" alt="The Cottage - Winter time" /></li>
<li><img src="http://exscale.se/__files/3d/ps2_small.jpg" alt="PS2" /></li>
</ul>
@exampleJS:
$('#jquery-drag-to-select-example').dragToSelect({
selectables: 'li',
onHide: function () {
alert($('#jquery-drag-to-select-example li.selected').length + ' selected');
}
});
*/
$.fn.dragToSelect = function (conf) {
var c = typeof(conf) == 'object' ? conf : {};
// Config
var config = $.extend({
className: 'pf-map-drag-to-select',
activeClass: 'active',
disabledClass: 'disabled',
selectedClass: 'pf-system-selected',
ignoreLockedClass: 'pf-system-locked', // do not select locked systems
ignoreVisibleClass: 'pf-system-hidden', // do not select invisible systems
scrollTH: 10,
percentCovered: 25,
selectables: false,
autoScroll: false,
selectOnMove: false,
onShow: function () {return true;},
onHide: function () {return true;},
onRefresh: function () {return true;}
}, c);
var realParent = $(this);
var parent = realParent;
// container for lasso element
// -> the only reason for NOT using the .pf-map is because of the zoom [scale()] feature or .pf-map
var lassoContainer = realParent.parent();
var animationFrameId;
var mouseIsDown = false;
var lastMousePosition = { x: 0, y: 0 };
// deselected items
var deselectedItems = $();
/*
do {
if (/auto|scroll|hidden/.test(parent.css('overflow'))) {
break;
}
parent = parent.parent();
} while (parent[0].parentNode);
*/
// Does user want to disable dragToSelect
if (conf == 'disable') {
parent.addClass(config.disabledClass);
return this;
}
else if (conf == 'enable') {
parent.removeClass(config.disabledClass);
return this;
}
var parentDim = {
left: 0,
top: 0,
width: 10,
height: 10
};
// set parent dimensions
// -> should be updated in case of left/right menu is open
var setParentDimensions = (parent) => {
var parentOffset = parent.offset();
parentDim = {
left: parentOffset.left,
top: parentOffset.top,
width: parent.width(),
height: parent.height()
};
}
setParentDimensions(parent);
// Current origin of select box
var selectBoxOrigin = {
left: 0,
top: 0
};
// Create select box
var selectBox = $('<div>')
.appendTo(lassoContainer)
.attr('class', config.className)
.css('position', 'absolute');
// Shows the select box
var showSelectBox = function (e) {
if (parent.is('.' + config.disabledClass)) {
return;
}
selectBoxOrigin.left = e.pageX - parentDim.left + parent[0].scrollLeft;
selectBoxOrigin.top = e.pageY - parentDim.top + parent[0].scrollTop;
var css = {
left: selectBoxOrigin.left + 'px',
top: selectBoxOrigin.top + 'px',
width: '1px',
height: '1px'
};
selectBox.addClass(config.activeClass).css(css);
config.onShow();
};
// Refreshes the select box dimensions and possibly position
var refreshSelectBox = function () {
var refreshed = false;
if (!selectBox.is('.' + config.activeClass) || parent.is('.' + config.disabledClass)) {
return refreshed;
}
var left = lastMousePosition.x - parentDim.left + parent[0].scrollLeft;
var top = lastMousePosition.y - parentDim.top + parent[0].scrollTop;
var tempWidth = selectBoxOrigin.left - left;
var tempHeight = selectBoxOrigin.top - top;
let newLeft = selectBoxOrigin.left;// - leftScroll;
let newTop = selectBoxOrigin.top;// - topScroll;
var newWidth = left - selectBoxOrigin.left;
var newHeight = top - selectBoxOrigin.top;
if(newWidth < 0){
newLeft = newLeft - tempWidth;
newWidth = newWidth * -1;
}
if(newHeight < 0){
newTop = newTop - tempHeight;
newHeight = newHeight * -1;
}
// check if dimension has changed -> save performance
var dimensionHash = [newWidth, newHeight].join('_');
if(selectBox.data('dimension-hash') !== dimensionHash){
selectBox.data('dimension-hash', dimensionHash);
var css = {
left: newLeft + 'px',
top: newTop + 'px',
width: newWidth + 'px',
height: newHeight + 'px'
};
selectBox.css(css);
config.onRefresh();
refreshed = true;
}
return refreshed;
};
// Hides the select box
var hideSelectBox = function () {
if (!selectBox.is('.' + config.activeClass) || parent.is('.' + config.disabledClass)) {
return;
}
if (config.onHide(selectBox, deselectedItems) !== false) {
selectBox.removeClass(config.activeClass);
}
};
// Selects all the elements in the select box's range
var selectElementsInRange = function () {
if (!selectBox.is('.' + config.activeClass) || parent.is('.' + config.disabledClass)) {
return;
}
var selectables = realParent.find(config.selectables + ':not(.' + config.ignoreLockedClass + ')'+ ':not(.' + config.ignoreVisibleClass + ')');
var selectBoxOffset = selectBox.offset();
var selectBoxDim = {
left: selectBoxOffset.left,
top: selectBoxOffset.top,
width: selectBox.width(),
height: selectBox.height()
};
selectables.each(function (i) {
var el = $(this);
var elOffset = el.offset();
var elDim = {
left: elOffset.left,
top: elOffset.top,
width: el.width(),
height: el.height()
};
if (percentCovered(selectBoxDim, elDim) > config.percentCovered) {
el.addClass(config.selectedClass);
// remove element from "deselected" elements (e.g on add -> remove -> add scenario)
deselectedItems = deselectedItems.not(el);
}else {
if(el.hasClass(config.selectedClass)){
el.removeClass(config.selectedClass);
deselectedItems = deselectedItems.add(el);
}
}
});
};
// Returns the amount (in %) that dim1 covers dim2
var percentCovered = function (dim1, dim2) {
// The whole thing is covering the whole other thing
if (
(dim1.left <= dim2.left) &&
(dim1.top <= dim2.top) &&
((dim1.left + dim1.width) >= (dim2.left + dim2.width)) &&
((dim1.top + dim1.height) > (dim2.top + dim2.height))
) {
return 100;
}
// Only parts may be covered, calculate percentage
else {
dim1.right = dim1.left + dim1.width;
dim1.bottom = dim1.top + dim1.height;
dim2.right = dim2.left + dim2.width;
dim2.bottom = dim2.top + dim2.height;
var l = Math.max(dim1.left, dim2.left);
var r = Math.min(dim1.right, dim2.right);
var t = Math.max(dim1.top, dim2.top);
var b = Math.min(dim1.bottom, dim2.bottom);
if (b >= t && r >= l) {
/* $('<div/>').appendTo(document.body).css({
background: 'red',
position: 'absolute',
left: l + 'px',
top: t + 'px',
width: (r - l) + 'px',
height: (b - t) + 'px',
zIndex: 100
}); */
var percent = (((r - l) * (b - t)) / (dim2.width * dim2.height)) * 100;
// alert(percent + '% covered')
return percent;
}
}
// Nothing covered, return 0
return 0;
};
// Event functions ----------------------------------------------------------------------------
var mousemoveCallback = function(){
if(mouseIsDown){
var refreshed = refreshSelectBox();
if(refreshed && config.selectables && config.selectOnMove){
selectElementsInRange();
}
// recursive re-call on next render
animationFrameId = requestAnimationFrame(mousemoveCallback);
}
}
var mouseupCallback = function(){
if(config.selectables){
selectElementsInRange();
}
hideSelectBox();
// stop animation frame and "reset" to default
cancelAnimationFrame(animationFrameId);
mouseIsDown = false;
// reset deselected item array
deselectedItems = $();
}
// Do the right stuff then return this --------------------------------------------------------
selectBox.mousemove(function(e){
setParentDimensions(parent);
lastMousePosition.x = e.pageX;
lastMousePosition.y = e.pageY;
e.preventDefault();
}).mouseup(mouseupCallback);
parent.mousedown(function(e){
if(
e.which === 1 && // left mouse down
e.target === realParent[0] // prevent while dragging a system :)
){
// Make sure user isn't clicking scrollbar (or disallow clicks far to the right actually)
if ((e.pageX + 20) > $(document.body).width()) {
return;
}
showSelectBox(e);
mouseIsDown = true;
animationFrameId = requestAnimationFrame(mousemoveCallback);
}
e.preventDefault();
}).mousemove(function(e){
setParentDimensions(parent);
lastMousePosition.x = e.pageX;
lastMousePosition.y = e.pageY;
e.preventDefault();
}).mouseup(mouseupCallback);
// Be nice
return this;
};

View File

@@ -1,9 +0,0 @@
/*!
* hoverIntent v1.9.0 // 2017.09.01 // jQuery v1.7.0+
* http://briancherne.github.io/jquery-hoverIntent/
*
* You may use hoverIntent under the terms of the MIT license. Basically that
* means you are free to use hoverIntent as long as this header is left intact.
* Copyright 2007-2017 Brian Cherne
*/
!function(factory){"use strict";"function"==typeof define&&define.amd?define(["jquery"],factory):jQuery&&!jQuery.fn.hoverIntent&&factory(jQuery)}(function($){"use strict";var cX,cY,_cfg={interval:100,sensitivity:6,timeout:0},INSTANCE_COUNT=0,track=function(ev){cX=ev.pageX,cY=ev.pageY},compare=function(ev,$el,s,cfg){if(Math.sqrt((s.pX-cX)*(s.pX-cX)+(s.pY-cY)*(s.pY-cY))<cfg.sensitivity)return $el.off(s.event,track),delete s.timeoutId,s.isActive=!0,ev.pageX=cX,ev.pageY=cY,delete s.pX,delete s.pY,cfg.over.apply($el[0],[ev]);s.pX=cX,s.pY=cY,s.timeoutId=setTimeout(function(){compare(ev,$el,s,cfg)},cfg.interval)},delay=function(ev,$el,s,out){return delete $el.data("hoverIntent")[s.id],out.apply($el[0],[ev])};$.fn.hoverIntent=function(handlerIn,handlerOut,selector){var instanceId=INSTANCE_COUNT++,cfg=$.extend({},_cfg);$.isPlainObject(handlerIn)?(cfg=$.extend(cfg,handlerIn),$.isFunction(cfg.out)||(cfg.out=cfg.over)):cfg=$.isFunction(handlerOut)?$.extend(cfg,{over:handlerIn,out:handlerOut,selector:selector}):$.extend(cfg,{over:handlerIn,out:handlerIn,selector:handlerOut});var handleHover=function(e){var ev=$.extend({},e),$el=$(this),hoverIntentData=$el.data("hoverIntent");hoverIntentData||$el.data("hoverIntent",hoverIntentData={});var state=hoverIntentData[instanceId];state||(hoverIntentData[instanceId]=state={id:instanceId}),state.timeoutId&&(state.timeoutId=clearTimeout(state.timeoutId));var mousemove=state.event="mousemove.hoverIntent.hoverIntent"+instanceId;if("mouseenter"===e.type){if(state.isActive)return;state.pX=ev.pageX,state.pY=ev.pageY,$el.off(mousemove,track).on(mousemove,track),state.timeoutId=setTimeout(function(){compare(ev,$el,state,cfg)},cfg.interval)}else{if(!state.isActive)return;$el.off(mousemove,track),state.timeoutId=setTimeout(function(){delay(ev,$el,state,cfg.out)},cfg.timeout)}};return this.on({"mouseenter.hoverIntent":handleHover,"mouseleave.hoverIntent":handleHover},cfg.selector)}});

View File

@@ -1,13 +1,7 @@
// Peity jQuery plugin version 3.2.1
// (c) 2016 Ben Pickles
// Peity jQuery plugin version 3.3.0
// (c) 2018 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 0<a?a:0});if("/"==a.delimiter)var e=b[0],b=[e,h.max(0,b[1]-e)];for(var c=0,e=b.length,t=0;c<e;c++)t+=b[c];t||(e=2,t=1,b=[0,1]);var l=2*a.radius,l=this.prepare(a.width||l,a.height||l),c=l.width(),f=l.height(),j=c/2,d=f/2,f=h.min(j,d),a=a.innerRadius;
"donut"==this.type&&!a&&(a=0.5*f);for(var r=h.PI,s=this.fill(),g=this.scale=function(a,b){var c=a/t*r*2-r/2;return[b*h.cos(c)+j,b*h.sin(c)+d]},m=0,c=0;c<e;c++){var u=b[c],i=u/t;if(0!=i){if(1==i)if(a)var i=j-0.01,p=d-f,n=d-a,i=q("path",{d:["M",j,p,"A",f,f,0,1,1,i,p,"L",i,n,"A",a,a,0,1,0,j,n].join(" ")});else i=q("circle",{cx:j,cy:d,r:f});else p=m+u,n=["M"].concat(g(m,f),"A",f,f,0,0.5<i?1:0,1,g(p,f),"L"),a?n=n.concat(g(p,a),"A",a,a,0,0.5<i?1:0,0,g(m,a)):n.push(j,d),m+=u,i=q("path",{d:n.join(" ")});
i.attr("fill",s.call(this,u,c,b));l.append(i)}}});d.register("donut",k.extend(!0,{},d.defaults.pie),function(a){d.graphers.pie.call(this,a)});d.register("line",{delimiter:",",fill:"#c6d9fd",height:16,min:0,stroke:"#4d89f9",strokeWidth:1,width:32},function(a){var b=this.values();1==b.length&&b.push(b[0]);for(var e=h.max.apply(h,a.max==v?b:b.concat(a.max)),c=h.min.apply(h,a.min==v?b:b.concat(a.min)),d=this.prepare(a.width,a.height),l=a.strokeWidth,f=d.width(),j=d.height()-l,k=e-c,e=this.x=function(a){return a*
(f/(b.length-1))},r=this.y=function(a){var b=j;k&&(b-=(a-c)/k*j);return b+l/2},s=r(h.max(c,0)),g=[0,s],m=0;m<b.length;m++)g.push(e(m),r(b[m]));g.push(f,s);a.fill&&d.append(q("polygon",{fill:a.fill,points:g.join(" ")}));l&&d.append(q("polyline",{fill:"none",points:g.slice(2,g.length-2).join(" "),stroke:a.stroke,"stroke-width":l,"stroke-linecap":"square"}))});d.register("bar",{delimiter:",",fill:["#4D89F9"],height:16,min:0,padding:0.1,width:32},function(a){for(var b=this.values(),e=h.max.apply(h,a.max==
v?b:b.concat(a.max)),c=h.min.apply(h,a.min==v?b:b.concat(a.min)),d=this.prepare(a.width,a.height),l=d.width(),f=d.height(),j=e-c,a=a.padding,k=this.fill(),r=this.x=function(a){return a*l/b.length},s=this.y=function(a){return f-(j?(a-c)/j*f:1)},g=0;g<b.length;g++){var m=r(g+a),u=r(g+1-a)-m,i=b[g],p=s(i),n=p,o;j?0>i?n=s(h.min(e,0)):p=s(h.max(c,0)):o=1;o=p-n;0==o&&(o=1,0<e&&j&&n--);d.append(q("rect",{fill:k.call(this,i,g,b),x:m,y:n,width:u,height:o}))}})})(jQuery,document,Math);
!function(t,i,e,n){var a=t.fn.peity=function(i,e){return l&&this.each(function(){var n=t(this),h=n.data("_peity");h?(i&&(h.type=i),t.extend(h.opts,e)):(h=new r(n,i,t.extend({},a.defaults[i],n.data("peity"),e)),n.change(function(){h.draw()}).data("_peity",h)),h.draw()}),this},r=function(t,i,e){this.$el=t,this.type=i,this.opts=e},h=r.prototype,s=h.svgElement=function(e,n){return t(i.createElementNS("http://www.w3.org/2000/svg",e)).attr(n)},l="createElementNS"in i&&s("svg",{})[0].createSVGRect;h.draw=function(){var t=this.opts;a.graphers[this.type].call(this,t),t.after&&t.after.call(this,t)},h.fill=function(){var i=this.opts.fill;return t.isFunction(i)?i:function(t,e){return i[e%i.length]}},h.prepare=function(t,i){return this.$svg||this.$el.hide().after(this.$svg=s("svg",{class:"peity"})),this.$svg.empty().data("_peity",this).attr({height:i,width:t})},h.values=function(){return t.map(this.$el.text().split(this.opts.delimiter),function(t){return parseFloat(t)})},a.defaults={},a.graphers={},a.register=function(t,i,e){this.defaults[t]=i,this.graphers[t]=e},a.register("pie",{fill:["#ff9900","#fff4dd","#ffc66e"],radius:8},function(i){if(!i.delimiter){var n=this.$el.text().match(/[^0-9\.]/);i.delimiter=n?n[0]:","}var a=t.map(this.values(),function(t){return t>0?t:0});if("/"==i.delimiter){var r=a[0],h=a[1];a=[r,e.max(0,h-r)]}for(var l=0,p=a.length,o=0;l<p;l++)o+=a[l];o||(p=2,o=1,a=[0,1]);var f=2*i.radius,c=this.prepare(i.width||f,i.height||f),u=c.width()/2,d=c.height()/2,g=e.min(u,d),v=i.innerRadius;"donut"!=this.type||v||(v=.5*g);var m=e.PI,y=this.fill(),w=this.scale=function(t,i){var n=t/o*m*2-m/2;return[i*e.cos(n)+u,i*e.sin(n)+d]},x=0;for(l=0;l<p;l++){var k,$=a[l],j=$/o;if(0!=j){if(1==j)if(v){var A=u-.01,E=d-g,F=d-v;k=s("path",{d:["M",u,E,"A",g,g,0,1,1,A,E,"L",A,F,"A",v,v,0,1,0,u,F].join(" "),"data-value":$})}else k=s("circle",{cx:u,cy:d,"data-value":$,r:g});else{var M=x+$,S=["M"].concat(w(x,g),"A",g,g,0,j>.5?1:0,1,w(M,g),"L");v?S=S.concat(w(M,v),"A",v,v,0,j>.5?1:0,0,w(x,v)):S.push(u,d),x+=$,k=s("path",{d:S.join(" "),"data-value":$})}k.attr("fill",y.call(this,$,l,a)),c.append(k)}}}),a.register("donut",t.extend(!0,{},a.defaults.pie),function(t){a.graphers.pie.call(this,t)}),a.register("line",{delimiter:",",fill:"#c6d9fd",height:16,min:0,stroke:"#4d89f9",strokeWidth:1,width:32},function(t){var i=this.values();1==i.length&&i.push(i[0]);for(var a=e.max.apply(e,t.max==n?i:i.concat(t.max)),r=e.min.apply(e,t.min==n?i:i.concat(t.min)),h=this.prepare(t.width,t.height),l=t.strokeWidth,p=h.width(),o=h.height()-l,f=a-r,c=this.x=function(t){return t*(p/(i.length-1))},u=this.y=function(t){var i=o;return f&&(i-=(t-r)/f*o),i+l/2},d=u(e.max(r,0)),g=[0,d],v=0;v<i.length;v++)g.push(c(v),u(i[v]));g.push(p,d),t.fill&&h.append(s("polygon",{fill:t.fill,points:g.join(" ")})),l&&h.append(s("polyline",{fill:"none",points:g.slice(2,g.length-2).join(" "),stroke:t.stroke,"stroke-width":l,"stroke-linecap":"square"}))}),a.register("bar",{delimiter:",",fill:["#4D89F9"],height:16,min:0,padding:.1,width:32},function(t){for(var i=this.values(),a=e.max.apply(e,t.max==n?i:i.concat(t.max)),r=e.min.apply(e,t.min==n?i:i.concat(t.min)),h=this.prepare(t.width,t.height),l=h.width(),p=h.height(),o=a-r,f=t.padding,c=this.fill(),u=this.x=function(t){return t*l/i.length},d=this.y=function(t){return p-(o?(t-r)/o*p:1)},g=0;g<i.length;g++){var v,m=u(g+f),y=u(g+1-f)-m,w=i[g],x=d(w),k=x,$=x;o?w<0?k=d(e.min(a,0)):$=d(e.max(r,0)):v=1,0==(v=$-k)&&(v=1,a>0&&o&&k--),h.append(s("rect",{"data-value":w,fill:c.call(this,w,g,i),x:m,y:k,width:y,height:v}))}})}(jQuery,document,Math);

View File

@@ -26,7 +26,7 @@
"gulp-requirejs-optimize": "1.3.x",
"gulp-sourcemaps": "^2.6.5",
"gulp-uglify": "^3.0.2",
"jshint": "^2.10.2",
"jshint": "^2.10.3",
"jshint-stylish": "^2.x.x",
"lodash.padend": "4.6.x",
"node-notifier": "^5.4.0",

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -1,2 +0,0 @@
"use strict";var mainScriptPath=document.body.getAttribute("data-script"),jsBaseUrl=document.body.getAttribute("data-js-path");requirejs.config({baseUrl:"js",paths:{conf:"app/conf",dialog:"app/ui/dialog",layout:"app/ui/layout",module:"app/ui/module",templates:"../../templates",img:"../../img",login:"./app/login",mappage:"./app/mappage",setup:"./app/setup",admin:"./app/admin",notification:"./app/notification",jquery:"lib/jquery-3.4.1.min",bootstrap:"lib/bootstrap.min",text:"lib/requirejs/text",mustache:"lib/mustache.min",localForage:"lib/localforage.min",velocity:"lib/velocity.min",velocityUI:"lib/velocity.ui.min",slidebars:"lib/slidebars",jsPlumb:"lib/jsplumb",farahey:"lib/farahey",customScrollbar:"lib/jquery.mCustomScrollbar.min",mousewheel:"lib/jquery.mousewheel.min",xEditable:"lib/bootstrap-editable.min",morris:"lib/morris.min",raphael:"lib/raphael.min",bootbox:"lib/bootbox.min",easyPieChart:"lib/jquery.easypiechart.min",peityInlineChart:"lib/jquery.peity.min",dragToSelect:"lib/jquery.dragToSelect",hoverIntent:"lib/jquery.hoverIntent.min",select2:"lib/select2.min",validator:"lib/validator.min",lazylinepainter:"lib/jquery.lazylinepainter-1.5.1.min",blueImpGallery:"lib/blueimp-gallery",blueImpGalleryHelper:"lib/blueimp-helper",blueImpGalleryBootstrap:"lib/bootstrap-image-gallery",bootstrapConfirmation:"lib/bootstrap-confirmation",bootstrapToggle:"lib/bootstrap-toggle.min",lazyload:"lib/jquery.lazyload.min",sortable:"lib/sortable.min","summernote.loader":"./app/summernote.loader",summernote:"lib/summernote/summernote.min",easePack:"lib/EasePack.min",tweenLite:"lib/TweenLite.min","datatables.loader":"./app/datatables.loader","datatables.net":"lib/datatables/DataTables-1.10.18/js/jquery.dataTables.min","datatables.net-buttons":"lib/datatables/Buttons-1.5.6/js/dataTables.buttons.min","datatables.net-buttons-html":"lib/datatables/Buttons-1.5.6/js/buttons.html5.min","datatables.net-responsive":"lib/datatables/Responsive-2.2.2/js/dataTables.responsive.min","datatables.net-select":"lib/datatables/Select-1.3.0/js/dataTables.select.min","datatables.plugins.render.ellipsis":"lib/datatables/plugins/render/ellipsis",pnotify:"lib/pnotify/pnotify","pnotify.buttons":"lib/pnotify/pnotify.buttons","pnotify.confirm":"lib/pnotify/pnotify.confirm","pnotify.nonblock":"lib/pnotify/pnotify.nonblock","pnotify.desktop":"lib/pnotify/pnotify.desktop","pnotify.history":"lib/pnotify/pnotify.history","pnotify.callbacks":"lib/pnotify/pnotify.callbacks","pnotify.reference":"lib/pnotify/pnotify.reference"},shim:{bootstrap:{deps:["jquery"]},farahey:{deps:["jsPlumb"]},velocity:{deps:["jquery"]},velocityUI:{deps:["velocity"]},slidebars:{deps:["jquery"]},customScrollbar:{deps:["jquery","mousewheel"]},"datatables.loader":{deps:["jquery"]},"datatables.net":{deps:["jquery"]},"datatables.net-buttons":{deps:["datatables.net"]},"datatables.net-buttons-html":{deps:["datatables.net-buttons"]},"datatables.net-responsive":{deps:["datatables.net"]},"datatables.net-select":{deps:["datatables.net"]},"datatables.plugins.render.ellipsis":{deps:["datatables.net"]},xEditable:{deps:["bootstrap"]},bootbox:{deps:["jquery","bootstrap"],exports:"bootbox"},morris:{deps:["jquery","raphael"],exports:"Morris",init:function(e,t){window.Raphael=t}},pnotify:{deps:["jquery"]},easyPieChart:{deps:["jquery"]},peityInlineChart:{deps:["jquery"]},dragToSelect:{deps:["jquery"]},hoverIntent:{deps:["jquery"]},select2:{deps:["jquery","mousewheel"],exports:"Select2"},validator:{deps:["jquery","bootstrap"]},lazylinepainter:{deps:["jquery","bootstrap"]},blueImpGallery:{deps:["jquery"]},bootstrapConfirmation:{deps:["bootstrap"]},bootstrapToggle:{deps:["jquery"]},lazyload:{deps:["jquery"]},summernote:{deps:["jquery"]}}}),require.config({baseUrl:jsBaseUrl}),requirejs([mainScriptPath]);
//# sourceMappingURL=app.js.map

Binary file not shown.

View File

@@ -1 +0,0 @@
{"version":3,"sources":["app.js"],"names":["mainScriptPath","document","body","getAttribute","jsBaseUrl","requirejs","config","baseUrl","paths","conf","dialog","layout","module","templates","img","login","mappage","setup","admin","notification","jquery","bootstrap","text","mustache","localForage","velocity","velocityUI","slidebars","jsPlumb","farahey","customScrollbar","mousewheel","xEditable","morris","raphael","bootbox","easyPieChart","peityInlineChart","dragToSelect","hoverIntent","select2","validator","lazylinepainter","blueImpGallery","blueImpGalleryHelper","blueImpGalleryBootstrap","bootstrapConfirmation","bootstrapToggle","lazyload","sortable","summernote.loader","summernote","easePack","tweenLite","datatables.loader","datatables.net","datatables.net-buttons","datatables.net-buttons-html","datatables.net-responsive","datatables.net-select","datatables.plugins.render.ellipsis","pnotify","pnotify.buttons","pnotify.confirm","pnotify.nonblock","pnotify.desktop","pnotify.history","pnotify.callbacks","pnotify.reference","shim","deps","exports","init","$","Raphael","window","require"],"mappings":"AAAA,aAGA,IAAIA,eAAiBC,SAASC,KAAKC,aAAa,eAI5CC,UAAYH,SAASC,KAAKC,aAAa,gBAG3CE,UAAUC,QACNC,QAAS,KAETC,OACIC,KAAM,WACNC,OAAQ,gBACRC,OAAQ,gBACRC,OAAQ,gBAERC,UAAW,kBACXC,IAAK,YAGLC,MAAO,cACPC,QAAS,gBACTC,MAAO,cACPC,MAAO,cACPC,aAAc,qBAEdC,OAAQ,uBACRC,UAAW,oBACXC,KAAM,qBACNC,SAAU,mBACVC,YAAa,sBACbC,SAAU,mBACVC,WAAY,sBACZC,UAAW,gBACXC,QAAS,cACTC,QAAS,cACTC,gBAAiB,kCACjBC,WAAY,4BACZC,UAAW,6BACXC,OAAQ,iBACRC,QAAS,kBACTC,QAAS,kBACTC,aAAc,8BACdC,iBAAkB,uBAClBC,aAAc,0BACdC,YAAa,6BACbC,QAAS,kBACTC,UAAW,oBACXC,gBAAiB,uCACjBC,eAAgB,sBAChBC,qBAAsB,qBACtBC,wBAAyB,8BACzBC,sBAAuB,6BACvBC,gBAAiB,2BACjBC,SAAU,0BACVC,SAAU,mBAEVC,oBAAqB,0BACrBC,WAAc,gCAGdC,SAAU,mBACVC,UAAW,oBAGXC,oBAAqB,0BACrBC,iBAAkB,6DAClBC,yBAA0B,yDAC1BC,8BAA+B,oDAC/BC,4BAA6B,+DAC7BC,wBAAyB,uDACzBC,qCAAsC,yCAGtCC,QAAS,sBACTC,kBAAmB,8BACnBC,kBAAmB,8BACnBC,mBAAoB,+BACpBC,kBAAmB,8BACnBC,kBAAmB,8BACnBC,oBAAqB,gCACrBC,oBAAqB,iCAEzBC,MACIhD,WACIiD,MAAO,WAEXzC,SACIyC,MAAO,YAEX7C,UACI6C,MAAO,WAEX5C,YACI4C,MAAO,aAEX3C,WACI2C,MAAO,WAEXxC,iBACIwC,MAAO,SAAU,eAErBhB,qBACIgB,MAAO,WAEXf,kBACIe,MAAO,WAEXd,0BACIc,MAAO,mBAEXb,+BACIa,MAAO,2BAEXZ,6BACIY,MAAO,mBAEXX,yBACIW,MAAO,mBAEXV,sCACIU,MAAO,mBAEXtC,WACIsC,MAAO,cAEXnC,SACImC,MAAO,SAAU,aACjBC,QAAS,WAEbtC,QACIqC,MAAO,SAAU,WACjBC,QAAS,SACTC,KAAM,SAAUC,EAAGC,GACfC,OAAOD,QAAUA,IAGzBb,SACIS,MAAO,WAEXlC,cACIkC,MAAO,WAEXjC,kBACIiC,MAAO,WAEXhC,cACIgC,MAAO,WAEX/B,aACI+B,MAAO,WAEX9B,SACI8B,MAAO,SAAU,cACjBC,QAAS,WAEb9B,WACI6B,MAAO,SAAU,cAErB5B,iBACI4B,MAAO,SAAU,cAErB3B,gBACI2B,MAAO,WAEXxB,uBACIwB,MAAO,cAEXvB,iBACIuB,MAAO,WAEXtB,UACIsB,MAAO,WAEXnB,YACImB,MAAO,cAQnBM,QAAQtE,QACJC,QAASH,YAIbC,WAAYL","file":"app.js","sourceRoot":"/js"}

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More