scripts/apps/dashboard/world-clock/world-clock.ts
import _ from 'lodash';
import './world-clock.scss';
import d3 from 'd3';
import {gettext} from 'core/utils';
import {getTimezoneLabel} from './timezones-all-labels';
angular.module('superdesk.apps.dashboard.world-clock', [
'superdesk.apps.dashboard', 'superdesk.core.datetime',
])
/**
* @ngdoc controller
* @module superdesk.apps.dashboard
* @name WorldClockConfigController
* @description
* Controller for the world clock widget configuration modal.
*/
.controller('WorldClockConfigController', ['$scope', 'notify', 'tzdata',
function($scope, notify, tzdata) {
$scope.availableZones = [];
$scope.getTimezoneLabel = getTimezoneLabel;
tzdata.$promise.then(() => {
$scope.availableZones = tzdata.getTzNames();
});
$scope.searchZones = function(searchString) {
if (searchString) {
$scope.availableZones = tzdata
.getTzNames()
.filter((tz) =>
getTimezoneLabel(tz)
.toLowerCase()
.includes(searchString.toLowerCase()),
);
} else {
$scope.availableZones = tzdata.getTzNames();
}
};
$scope.notify = function(action, zone) {
if (action === 'add') {
notify.success(gettext('World clock added: {{zone}}', {zone}), 3000);
} else if (action === 'remove') {
notify.success(gettext('World clock removed: {{zone}}', {zone}), 3000);
}
};
$scope.notIn = function(haystack) {
return function(needle) {
return haystack.indexOf(needle) === -1;
};
};
$scope.configuration.zones = $scope.configuration.zones || [];
}])
/**
* @ngdoc controller
* @module superdesk.apps.dashboard
* @name WorldClockController
* @description
* Controller for the directive - the one that creates
* a dashboard widget for displaying the current time in different
* time zones around the world.
*/
.controller('WorldClockController', ['$scope', '$interval', 'tzdata', 'moment',
function($scope, $interval, tzdata, moment) {
var interval, INTERVAL_DELAY = 500;
function updateUTC() {
$scope.utc = moment();
$scope.$digest();
}
// XXX: a hack-ish workaround to expose the object loaded via
// RequireJS to the testing code which does not use the latter
this._moment = moment;
tzdata.$promise.then(() => {
moment.tz.add(
_.pick(tzdata, ['zones', 'links']),
);
});
interval = $interval(updateUTC, INTERVAL_DELAY, 0, false);
$scope.$on('$destroy', function stopTimeout() {
$interval.cancel(interval);
});
}])
/**
* sdClock analog clock
*/
.directive('sdClock', () => {
var pi = Math.PI,
scales = {
s: d3.scale
.linear()
.domain([0, 59 + 999 / 1000])
.range([0, 2 * pi]),
m: d3.scale
.linear()
.domain([0, 59 + 59 / 60])
.range([0, 2 * pi]),
h: d3.scale
.linear()
.domain([0, 11 + 59 / 60])
.range([0, 2 * pi]),
};
return {
scope: {
utc: '=',
tz: '@',
},
link: function(scope, element, attrs) {
var width = 105,
height = 100,
r = Math.min(width, height) * 0.8 * 0.5,
dayBg = '#d4d8de',
dayClockhands = '#313131',
dayNumbers = '#a0a0a0',
nightBg = '#1b1e23',
nightClockhands = '#e0e0e0',
nightNumbers = '#848484';
var svg = d3.select(element[0])
.append('svg')
.attr('width', width)
.attr('height', height);
var clock = svg.append('g')
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
// background circle
clock.append('circle')
.attr('r', r)
.attr('class', 'clock-outer')
.style('stroke-width', 1.5);
// inner dot
clock.append('circle')
.attr('r', 1.5)
.attr('class', 'clock-inner');
// numbers
clock.selectAll('.number-lines')
.data(_.range(0, 59, 5))
.enter()
.append('path')
.attr('d', (d) => {
var angle = scales.m(d);
var arc = d3.svg.arc()
.innerRadius(r * 0.7)
.outerRadius(r * 0.9)
.startAngle(angle)
.endAngle(angle);
return arc();
})
.attr('class', 'number-lines')
.style('stroke-width', 1.5);
// format data for given time
function getData(timeStr) {
var time = timeStr.split(':');
return [
{unit: 'h', val: parseInt(time[0], 10) + parseInt(time[1], 10) / 60, r: 0.5},
{unit: 'm', val: parseInt(time[1], 10), r: 0.8},
];
}
scope.$watch('utc', (utc) => {
var time = utc ? utc.tz(scope.tz).format('HH:mm:ss') : '00:00:00';
var data = getData(time);
var isDay = data[0].val >= 8 && data[0].val < 20;
if (isDay) {
clock.selectAll('.clock-outer').style('fill', dayBg);
clock.selectAll('.clock-inner').style('fill', dayBg);
clock.selectAll('.number-lines').style('stroke', dayNumbers);
} else {
clock.selectAll('.clock-outer').style('fill', nightBg);
clock.selectAll('.clock-inner').style('fill', nightBg);
clock.selectAll('.number-lines').style('stroke', nightNumbers);
}
clock.selectAll('.clockhand').remove();
clock.selectAll('.clockhand')
.data(data)
.enter()
.append('path')
.attr('d', (d) => {
var angle = scales[d.unit](d.val);
var arc = d3.svg.arc()
.innerRadius(r * 0)
.outerRadius(r * d.r)
.startAngle(angle)
.endAngle(angle);
return arc();
})
.attr('class', 'clockhand')
.style('stroke-width', 2)
.style('stroke', isDay ? dayClockhands : nightClockhands);
});
},
};
})
.config(['dashboardWidgetsProvider', function(dashboardWidgets) {
dashboardWidgets.addWidget('world-clock', {
label: gettext('World Clock'),
multiple: true,
icon: 'time',
max_sizex: 2,
max_sizey: 1,
sizex: 1,
sizey: 1,
classes: 'tabs modal--nested-fix',
thumbnail: 'scripts/apps/dashboard/world-clock/thumbnail.svg',
template: 'scripts/apps/dashboard/world-clock/widget-worldclock.html',
configurationTemplate: 'scripts/apps/dashboard/world-clock/configuration.html',
configuration: {zones: ['Europe/London', 'Asia/Tokyo', 'Europe/Moscow']},
description: gettext('World clock widget'),
});
}]);