PDF rausgenommen

This commit is contained in:
aschwarz
2023-01-23 11:03:31 +01:00
parent 82d562a322
commit a6523903eb
28078 changed files with 4247552 additions and 2 deletions

View File

@@ -0,0 +1,693 @@
/*!
* Matomo - free/libre analytics platform
*
* Real time visitors map
* Using Kartograph.js http://kartograph.org/
*
* @link https://matomo.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
(function () {
var UIControl = require('piwik/UI').UIControl;
var RealtimeMap = window.UserCountryMap.RealtimeMap = function (element) {
UIControl.call(this, element);
this._init();
this.run();
};
RealtimeMap.initElements = function () {
UIControl.initElements(this, '.RealTimeMap');
};
$.extend(RealtimeMap.prototype, UIControl.prototype, {
_init: function () {
var $element = this.$element;
this.config = JSON.parse($element.attr('data-config'));
// If the map is loaded from the menu, do a few tweaks to clean up the display
if ($element.attr('data-standalone') == 1) {
this._initStandaloneMap();
}
// handle widgetry
if ($('#dashboardWidgetsArea').length) {
var $widgetContent = $element.closest('.widgetContent');
var self = this;
$widgetContent.on('widget:maximise', function () {
self.resize();
}).on('widget:minimise', function () {
self.resize();
});
}
// set unique ID for kartograph map div
this.uniqueId = 'RealTimeMap_map-' + this._controlId;
$('.RealTimeMap_map', $element).attr('id', this.uniqueId);
// create the map
this.map = $K.map('#' + this.uniqueId);
$element.focus();
},
_initStandaloneMap: function () {
$('#periodString').hide();
initTopControls();
var $rootScope = piwikHelper.getAngularDependency('$rootScope');
$rootScope.$on('piwikPageChange', function () {
var href = location.href;
var clickedMenuIsNotMap = !href || (href.indexOf('module=UserCountryMap&action=realtimeWorldMap') == -1);
if (clickedMenuIsNotMap) {
$('#periodString').show();
initTopControls();
}
});
$('.realTimeMap_overlay').css('top', '0px');
$('.realTimeMap_datetime').css('top', '20px');
},
run: function () {
var self = this,
config = self.config,
_ = config._,
map = self.map,
maxVisits = config.maxVisits || 100,
changeVisitAlpha = typeof config.changeVisitAlpha === 'undefined' ? true : config.changeVisitAlpha,
removeOldVisits = typeof config.removeOldVisits === 'undefined' ? true : config.removeOldVisits,
doNotRefreshVisits = typeof config.doNotRefreshVisits === 'undefined' ? false : config.doNotRefreshVisits,
enableAnimation = typeof config.enableAnimation === 'undefined' ? true : config.enableAnimation,
forceNowValue = typeof config.forceNowValue === 'undefined' ? false : +config.forceNowValue,
lastTimestamp = -1,
lastVisits = [],
visitSymbols,
tokenAuth = '' + config.reqParams.token_auth,
oldest,
isFullscreenWidget = $('.widget').parent().get(0) == document.body,
now,
nextReqTimer,
symbolFadeInTimer = [],
colorMode = 'default',
currentMap = 'world',
yesterday = false,
userHasZoomed = false,
colorManager = piwik.ColorManager,
colors = colorManager.getColors('realtime-map', ['white-bg', 'white-fill', 'black-bg', 'black-fill', 'visit-stroke',
'website-referrer-color', 'direct-referrer-color', 'search-referrer-color',
'live-widget-highlight', 'live-widget-unhighlight', 'symbol-animate-fill', 'region-stroke-color']),
currentTheme = 'white',
colorTheme = {
white: {
bg: colors['white-bg'],
fill: colors['white-fill']
},
black: {
bg: colors['black-bg'],
fill: colors['black-fill']
}
},
visitStrokeColor = colors['visit-stroke'],
referrerColorWebsite = colors['referrer-color-website'],
referrerColorDirect = colors['referrer-color-direct'],
referrerColorSearch = colors['referrer-color-search'],
liveWidgetHighlightColor = colors['live-widget-highlight'],
liveWidgetUnhighlightColor = colors['live-widget-unhighlight'],
symbolAnimateFill = colors['symbol-animate-fill']
;
self.widget = $('#widgetRealTimeMaprealtimeMap').parent();
var preset = self.widget.dashboardWidget('getWidgetObject').parameters;
if (preset) {
currentTheme = preset.colorTheme;
colorMode = preset.colorMode;
currentMap = preset.lastMap;
}
/*
* returns the parameters for API calls, extended from
* self.reqParams which is set in template
*/
function _reportParams(firstRun) {
return $.extend(config.reqParams, {
module: 'API',
method: 'Live.getLastVisitsDetails',
filter_limit: maxVisits,
showColumns: ['latitude', 'longitude', 'actions', 'lastActionTimestamp',
'visitLocalTime', 'city', 'country', 'countryCode', 'referrerType', 'referrerName',
'referrerTypeName', 'browserIcon', 'operatingSystemIcon', 'deviceType', 'deviceModel',
'countryFlag', 'idVisit', 'actionDetails', 'continentCode',
'actions', 'searches', 'goalConversions', 'visitorId', 'userId'].join(','),
minTimestamp: firstRun ? 0 : lastTimestamp
});
}
/*
* wrapper around jQuery.ajax, moves token_auth parameter
* to POST data while keeping other parameters as GET
*/
function ajax(params) {
delete params['token_auth'];
return $.ajax({
url: 'index.php?' + $.param(params),
dataType: 'json',
data: { token_auth: tokenAuth },
type: 'POST'
});
}
/*
* updateMap is called by renderCountryMap() and renderWorldMap()
*/
function _updateMap(svgUrl, callback) {
if (svgUrl === undefined) return;
map.loadMap(config.svgBasePath + svgUrl, function () {
map.clear();
self.resize();
callback();
$('.ui-tooltip').remove(); // remove all existing tooltips
}, { padding: -3});
}
/*
* to ensure that onResize is not called a hundred times
* while resizing the browser window, this functions
* makes sure to only call onResize at the end
*/
function onResizeLazy() {
clearTimeout(self._resizeTimer);
self._resizeTimer = setTimeout(self.resize.bind(self), 300);
}
/*
* returns value betwddn 0..1, where 1 means that the
* visit is fresh, and 0 means the visit is almost gone
* from the map
*/
function age(r) {
var nowSecs = Math.floor(now);
var o = (r.lastActionTimestamp - oldest) / (nowSecs - oldest);
return Math.min(1, Math.max(0, o));
}
function relativeTime(ds) {
var val = function (val) { return '<strong>' + Math.round(val) + '</strong>'; };
return (ds < 90 ? _.seconds_ago.replace('%s', val(ds))
: ds < 5400 ? _.minutes_ago.replace('%s', val(ds / 60))
: ds < 129600 ? _.hours_ago.replace('%s', val(ds / 3600))
: _.days_ago.replace('%s', val(ds / 86400)));
}
/*
* returns the content of the tooltip displayed for each
* visitor on the map
*/
function visitTooltip(r) {
var ds = new Date().getTime() / 1000 - r.lastActionTimestamp,
ad = r.actionDetails,
ico = function (src) { return '<img height="16px" src="' + src + '" alt="" class="icon" />&nbsp;'; };
return '<h3>' + (r.city ? $('<span>').text(r.city).html() + ' / ' : '') + $('<span>').text(r.country).html() + '</h3>' +
// icons
ico(r.countryFlag) + ico(r.browserIcon) + ico(r.operatingSystemIcon) + '<br/>' +
// device type, model, brand
r.deviceType + ' (' + r.deviceModel + ')<br/>' +
// User ID
(r.userId ? _pk_translate('General_UserId') + ':&nbsp;' + $('<span>').text(r.userId).html() + '<br/>' : '') +
// last action
(ad && ad.length && ad[ad.length - 1].pageTitle ? '' + $('<span>').text(ad[ad.length - 1].pageTitle).html() + '<br/>' : '') +
// time of visit
'<div class="rel-time" data-actiontime="' + r.lastActionTimestamp + '">' + relativeTime(ds) + '</div>' +
// either from or direct
(r.referrerType == "direct" ? r.referrerTypeName :
_.from + ': ' + $('<span>').text(r.referrerName).html()) + '<br />' +
// local time
'<small>' + _.local_time + ': ' + r.visitLocalTime + '</small><br />' +
// goals, if available
(self.config.siteHasGoals && r.goalConversions ? '<small>' + _.goal_conversions.replace('%s', '<strong>' + r.goalConversions + '</strong>') +
(r.searches > 0 ? ', ' + _.searches.replace('%s', r.searches) : '') + '</small><br />' : '') +
// actions and searches
'<small>' + _.actions.replace('%s', '<strong>' + r.actions + '</strong>') +
(r.searches > 0 ? ', ' + _.searches.replace('%s', '<strong>' + r.searches + '</strong>') : '') + '</small>';
}
/*
* the radius of the symbol depends on the lastActionTimestamp
*/
function visitRadius(r) {
return Math.pow(age(r), 4) * (self.maxRad - self.minRad) + self.minRad;
}
/*
* defines the color of the map symbols.
* depends on colorMode, which is set to 'default'
* unless you type Shift+Alt+C
*/
function visitColor(r) {
var col,
engaged = self.config.siteHasGoals ? r.goalConversions > 0 : r.actions > 4;
if (colorMode == 'referrerType') {
col = ({
website: referrerColorWebsite,
direct: referrerColorDirect,
search: referrerColorSearch
})[r.referrerType];
}
// defu
else col = chroma.hsl(
42 * age(r), // hue
Math.sqrt(age(r)), // saturation
(engaged ? 0.65 : 0.5) - (1 - age(r)) * 0.45 // lightness
);
return col;
}
/*
* attributes of the map symbols
*/
function visitSymbolAttrs(r) {
var result = {
fill: visitColor(r).hex(),
stroke: visitStrokeColor,
'stroke-width': 1 * age(r),
r: visitRadius(r),
cursor: 'pointer'
};
if (changeVisitAlpha) {
result['fill-opacity'] = Math.pow(age(r), 2) * 0.8 + 0.2;
result['stroke-opacity'] = Math.pow(age(r), 1.7) * 0.8 + 0.2;
}
return result;
}
/*
* eventually highlights the row in LiveVisitors widget
* that corresponds to a visit on the map
*/
function highlightVisit(r) {
$('#visitsLive').find('li#' + r.idVisit + ' .datetime')
.css('background', liveWidgetHighlightColor);
}
/*
* removes the highlight after the mouse left
* the visit marker on the map
*/
function unhighlightVisit(r) {
$('#visitsLive').find('li#' + r.idVisit + ' .datetime')
.css({ background: liveWidgetUnhighlightColor });
}
/*
* create a nice popping animation for appearing
* visit symbols.
*/
function animateSymbol(s) {
// create a white outline and explode it
var c = map.paper.circle().attr(s.path.attrs);
c.insertBefore(s.path);
c.attr({ fill: false });
c.animate({ r: c.attrs.r * 3, 'stroke-width': 7, opacity: 0 }, 2500,
'linear', function () { c.remove(); });
// ..and pop the bubble itself
var col = s.path.attrs.fill,
rad = s.path.attrs.r;
s.path.show();
s.path.attr({ fill: symbolAnimateFill, r: 0.1, opacity: 1 });
s.path.animate({ fill: col, r: rad }, 700, 'bounce');
}
// default click behavior. if a visit is clicked, the visitor profile is launched,
// otherwise zoom in or out.
// TODO: visitor profile launching logic should probably be contained in
// visitorProfile.js. not sure how to do that, though...
this.$element.on('mapClick', function (e, visit, mapPath) {
var VisitorProfileControl = require('piwik/UI').VisitorProfileControl;
if (visit
&& VisitorProfileControl
&& !self.$element.closest('.visitor-profile').length
) {
VisitorProfileControl.showPopover(visit.visitorId);
} else {
var cont = UserCountryMap.cont2cont[mapPath.data.continentCode];
if (cont && cont != currentMap) {
updateMap(cont);
}
}
});
/*
* this function requests new data from Live.getLastVisitsDetails
* and updates the symbols on the map. Then, it sets a timeout
* to call itself after the refresh time set by Matomo
*
* If firstRun is true, the SymbolGroup is initialized
*/
function refreshVisits(firstRun) {
if (lastTimestamp != -1
&& doNotRefreshVisits
&& !firstRun
) {
return;
}
/*
* this is called after new visit reports came in
*/
function gotNewReport(report) {
// if the map has been destroyed, do nothing
if (!self.map || !self.$element.length || !$.contains(document, self.$element[0])) {
return;
}
// successful request, so set timeout for next API call
nextReqTimer = setTimeout(refreshVisits, config.liveRefreshAfterMs);
// hide loading indicator
$('.realTimeMap_overlay img').hide();
$('.realTimeMap_overlay .loading_data').hide();
// store current timestamp
now = forceNowValue || (new Date().getTime() / 1000);
if (firstRun) { // if we run this the first time, we initialiize the map symbols
visitSymbols = map.addSymbols({
data: [],
type: $K.Bubble,
/*title: function(d) {
return visitRadius(d) > 15 && d.actions > 1 ? d.actions : '';
},
labelattrs: {
fill: '#fff',
'font-weight': 'bold',
'font-size': 11,
stroke: false,
cursor: 'pointer'
},*/
sortBy: function (r) { return r.lastActionTimestamp; },
radius: visitRadius,
location: function (r) { return [r.longitude, r.latitude]; },
attrs: visitSymbolAttrs,
tooltip: visitTooltip,
mouseenter: highlightVisit,
mouseleave: unhighlightVisit,
click: function (visit, mapPath, evt) {
evt.stopPropagation();
self.$element.trigger('mapClick', [visit, mapPath]);
}
});
// clear existing report
lastVisits = [];
}
if (report.length) {
// filter results without location
report = $.grep(report, function (r) {
return r.latitude !== null;
});
// show warning if no visits w/ latitude
$('#realTimeMapNoVisitsInfo').toggle(!report.length);
}
// check wether we got any geolocated visits left
if (!report.length) {
$('.realTimeMap_overlay .showing_visits_of').hide();
$('.realTimeMap_overlay .no_data').show();
return;
} else {
$('.realTimeMap_overlay .showing_visits_of').show();
$('.realTimeMap_overlay .no_data').hide();
if (yesterday === false) {
yesterday = report[0].lastActionTimestamp - 24 * 60 * 60;
}
lastVisits = [].concat(report).concat(lastVisits).slice(0, maxVisits);
oldest = Math.max(lastVisits[lastVisits.length - 1].lastActionTimestamp, yesterday);
// let's try a different strategy
// remove symbols that are too old
var _removed = 0;
if (removeOldVisits) {
visitSymbols.remove(function (r) {
if (r.lastActionTimestamp < oldest) _removed++;
return r.lastActionTimestamp < oldest;
});
}
// update symbols that remain
visitSymbols.update({
radius: function (d) { return visitSymbolAttrs(d).r; },
attrs: visitSymbolAttrs
}, true);
// add new symbols
var newSymbols = [];
$.each(report, function (i, r) {
newSymbols.push(visitSymbols.add(r));
});
visitSymbols.layout().render();
if (enableAnimation) {
$.each(newSymbols, function (i, s) {
if (i > 10) return false;
s.path.hide(); // hide new symbol at first
var t = setTimeout(function () { animateSymbol(s); },
1000 * (s.data.lastActionTimestamp - now) + config.liveRefreshAfterMs);
symbolFadeInTimer.push(t);
});
}
lastTimestamp = report[0].lastActionTimestamp;
// show
var dur = lastTimestamp - oldest, d;
if (dur < 60) d = dur + ' ' + _.seconds;
else if (dur < 3600) d = Math.ceil(dur / 60) + ' ' + _.minutes;
else d = Math.ceil(dur / 3600) + ' ' + _.hours;
$('.realTimeMap_timeSpan').html(d);
if (!userHasZoomed) {
// we only apply auto zoom when user has not zoomed manually
var usedContinents = [];
var usedCountries = [];
var aSymbol;
for (var z = 0; z < visitSymbols.symbols.length; z++) {
aSymbol = visitSymbols.symbols[z];
if (aSymbol && aSymbol.data) {
if (aSymbol.data.continentCode && -1 === usedContinents.indexOf(aSymbol.data.continentCode)) {
usedContinents.push(aSymbol.data.continentCode);
}
if (aSymbol.data.countryCode && -1 === usedCountries.indexOf(aSymbol.data.countryCode)) {
usedCountries.push(aSymbol.data.countryCode);
}
}
}
if (usedCountries.length === 1 && usedCountries[0] && usedCountries[0] !== 'unk') {
updateMap(UserCountryMap.ISO2toISO3[usedCountries[0].toUpperCase()], false);
} else if (usedContinents.length === 1 && usedContinents[0] && usedContinents[0] !== 'unk') {
updateMap(UserCountryMap.cont2cont[usedContinents[0]], false);
}
}
}
firstRun = false;
}
if (firstRun && lastVisits.length) {
// zoom changed, use cached report data
gotNewReport(lastVisits.slice());
} else if (Visibility.hidden()) {
nextReqTimer = setTimeout(refreshVisits, config.liveRefreshAfterMs);
} else {
// request API for new data
$('.realTimeMap_overlay img').show();
ajax(_reportParams(firstRun)).done(gotNewReport);
}
}
/*
* Set up the base map after loading of the SVG. Adds a single layer
* that shows countries in gray with white outlines. Also this is where
* the zoom behaviour is initialized.
*/
function initMap() {
$('#widgetRealTimeMapliveMap .loadingPiwik, .RealTimeMap .loadingPiwik').hide();
map.addLayer(currentMap.length == 3 ? 'context' : 'countries', {
styles: {
fill: colorTheme[currentTheme].fill,
stroke: colorTheme[currentTheme].bg,
'stroke-width': 0.2
},
click: function (d, p, evt) {
evt.stopPropagation();
userHasZoomed = true;
if (currentMap.length == 2){ // zoom to country
updateMap(d.iso);
} else if (currentMap != 'world') { // zoom out if zoomed in
updateMap('world');
} else { // or zoom to continent view otherwise
updateMap(UserCountryMap.ISO3toCONT[d.iso]);
}
},
title: function (d) {
// return the country name for educational purpose
return d.name;
}
});
if (currentMap.length == 3){
map.addLayer('regions', {
styles: {
stroke: colors['region-stroke-color']
}
});
}
refreshVisits(true);
}
function storeSettings() {
self.widget.dashboardWidget('setParameters', {
lastMap: currentMap, theme: colorTheme, colorMode: colorMode
});
}
/*
* updates the map view (after changing the zoom)
* clears all existing timeouts
*/
function updateMap(_map, _storeSettings) {
if ('undefined' === typeof _storeSettings) {
_storeSettings = true;
}
if (_map && currentMap === _map && _map !== 'world') {
return;
}
clearTimeout(nextReqTimer);
$.each(symbolFadeInTimer, function (i, t) {
clearTimeout(t);
});
symbolFadeInTimer = [];
try {
map.removeSymbols();
} catch (e) {}
currentMap = _map;
_updateMap(currentMap + '.svg', initMap);
if (_storeSettings) {
storeSettings();
}
}
updateMap(location.hash && (location.hash == '#world' || location.hash.match(/^#[A-Z]{2,3}$/)) ? location.hash.substr(1) : 'world'); // TODO: restore last state
// clicking on map background zooms out
$('.RealTimeMap_map', this.$element).off('click').click(function () {
if (currentMap != 'world') {
userHasZoomed = true;
updateMap('world');
}
});
// secret gimmick shortcuts
this.$element.on('keydown', function (evt) {
// shift+alt+C changes color mode
if (evt.shiftKey && evt.altKey && evt.keyCode == 67) {
colorMode = ({
'default': 'referrerType',
referrerType: 'default'
})[colorMode];
storeSettings();
}
function switchTheme() {
self.$element.css({ background: colorTheme[currentTheme].bg });
if (isFullscreenWidget) {
$('body').css({ background: colorTheme[currentTheme].bg });
$('.widget').css({ 'border-width': 1 });
}
map.getLayer('countries')
.style('fill', colorTheme[currentTheme].fill)
.style('stroke', colorTheme[currentTheme].bg);
storeSettings();
}
// shift+alt+B: switch to black background
if (evt.shiftKey && evt.altKey && evt.keyCode == 66) {
currentTheme = 'black';
switchTheme();
}
// shift+alt+W: return to white background
if (evt.shiftKey && evt.altKey && evt.keyCode == 87) {
currentTheme = 'white';
switchTheme();
}
});
// make sure the map adapts to the widget size
$(window).on('resize.' + this.uniqueId, onResizeLazy);
// setup automatic tooltip updates
this._tooltipUpdateInterval = setInterval(function () {
$('.qtip .rel-time').each(function (i, el) {
el = $(el);
var ds = new Date().getTime() / 1000 - el.data('actiontime');
el.html(relativeTime(ds));
});
var d = new Date(), datetime = d.toTimeString().substr(0, 8);
$('.realTimeMap_datetime').html(datetime);
}, 1000);
},
/*
* resizes the map to widget dimensions
*/
resize: function () {
var ratio, w, h, map = this.map;
ratio = map.viewAB.width / map.viewAB.height;
w = map.container.width();
h = Math.min(w / ratio, $(window).height() - 30);
var radScale = Math.pow((h * ratio * h) / 130000, 0.3);
this.maxRad = 10 * radScale;
this.minRad = 4 * radScale;
map.container.height(h - 2);
map.resize(w, h);
if (map.symbolGroups && map.symbolGroups.length > 0) {
map.symbolGroups[0].update();
}
if (w < 355) $('.UserCountryMap .tableIcon span').hide();
else $('.UserCountryMap .tableIcon span').show();
},
_destroy: function () {
UIControl.prototype._destroy.call(this);
if (this._tooltipUpdateInterval) {
clearInterval(this._tooltipUpdateInterval);
}
$(window).off('resize.' + this.uniqueId);
this.map.clear();
$(this.map.container).html('');
delete this.map;
}
});
}());

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 it is too large Load Diff