1// Copyright 2008 Henry de Valence <hdevalence@gmail.com>
2//
3// This library is free software; you can redistribute it and/or
4// modify it under the terms of the GNU Lesser General Public
5// License as published by the Free Software Foundation; either
6// version 2.1 of the License, or (at your option) any later version.
7//
8// This library is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11// Lesser General Public License for more details.
12//
13// You should have received a copy of the GNU Lesser General Public
14// License along with this library. If not, see <http://www.gnu.org/licenses/>.
15
16
17//Mine
18#include "worldclock.h"
19
20//Qt
21#include <QPainter>
22#include <QRadialGradient>
23#include <QBrush>
24#include <QGraphicsSceneHoverEvent>
25#include <QList>
26#include <QSize>
27#include <QRect>
28#include <QTime>
29#include <QDate>
30#include <QDateTime>
31
32//KDE
33#include <KDebug>
34#include <KLocale>
35#include <KConfigDialog>
36#include <KConfigGroup>
37#include <KTimeZone>
38#include <KTimeZoneWidget>
39#include <KSystemTimeZone>
40
41//Plasma
42#include <Plasma/Applet>
43#include <Plasma/DataEngine>
44
45//Marble
46#include "MarbleGlobal.h"
47#include "MarbleMap.h"
48#include "MarbleModel.h"
49#include "AbstractFloatItem.h"
50#include "SunLocator.h"
51#include "GeoPainter.h"
52#include "LatLonEdit.h"
53#include "ViewportParams.h"
54
55namespace Marble
56{
57
58WorldClock::WorldClock(QObject *parent, const QVariantList &args)
59 : Plasma::Applet(parent, args),
60 m_showDate( false ),
61 m_customTz( false ),
62 m_map(0),
63 m_isHovered( false ),
64 m_timeEngine( 0 )
65{
66 KGlobal::locale()->insertCatalog("marble");
67 KGlobal::locale()->insertCatalog("marble_qt");
68 KGlobal::locale()->insertCatalog("timezones4");
69 setHasConfigurationInterface(true);
70 setAcceptHoverEvents(true);
71 //The applet needs a 2:1 ratio
72 //so that the map fits properly
73 resize(QSize(400, 200));
74}
75
76void WorldClock::init()
77{
78 KConfigGroup cg = config();
79 m_map = new MarbleMap();
80
81 //Set how we want the map to look
82 m_map->centerOn( cg.readEntry("rotation", -20), 0 );
83 m_map->setMapThemeId( "earth/bluemarble/bluemarble.dgml" );
84 m_map->setShowCompass ( false );
85 m_map->setShowClouds ( false );
86 m_map->setShowScaleBar ( false );
87 m_map->setShowGrid ( false );
88 m_map->setShowPlaces ( false );
89 m_map->setShowCities ( false );
90 m_map->setShowTerrain ( false );
91 m_map->setShowOtherPlaces( false );
92 // set the date time of the marble model otherwise the sun will not show up correctly
93 m_map->model()->setClockDateTime(QDateTime::currentDateTimeUtc());
94
95 if(cg.readEntry("projection", static_cast<int>(Equirectangular)) == Mercator)
96 m_map->setProjection(Mercator);
97 else
98 m_map->setProjection(Equirectangular);
99
100 foreach( RenderPlugin* item, m_map->renderPlugins() )
101 item->setVisible( false );
102
103 //Set up the Sun to draw night/day shadow
104 m_map->setShowSunShading(true);
105 m_map->setShowCityLights(true);
106 m_map->setLockToSubSolarPoint(cg.readEntry("centersun", false ));
107 m_map->setSubSolarPointIconVisible(true);
108
109 m_customTz = cg.readEntry("customtz", false );
110 m_locationkey = KSystemTimeZones::local().name();
111 if(m_customTz) {
112 QStringList tzlist = cg.readEntry("tzlist", QStringList());
113 m_locations = QMap<QString, KTimeZone>();
114 foreach( const QString& tzname, tzlist ) {
115 m_locations.insert(tzname, KSystemTimeZones::zone(tzname));
116 }
117 if(!m_locations.contains(m_locationkey))
118 m_locationkey = m_locations.keys().first();
119 } else {
120 m_locations = KSystemTimeZones::zones();
121 QList<QString> zones = m_locations.keys();
122 for (int i = 0; i < zones.size(); ++i ) {
123 KTimeZone curzone = m_locations.value( zones.at( i ) );
124 if ( curzone.latitude() == KTimeZone::UNKNOWN ||
125 curzone.longitude() == KTimeZone::UNKNOWN ) {
126 m_locations.remove( zones.at(i) );
127 }
128 }
129 }
130
131 //Font sizes will change before painting
132 m_timeFont = QFont( "Helvetica", 12, QFont::Bold);
133 m_locationFont = QFont( "Helvetica", 12, QFont::Bold);
134 m_points = QHash<QString, QPoint>();
135 m_lastRect = QRect(0,0,0,0);
136 m_showDate = cg.readEntry("showdate", false);
137
138 setTz( getZone() );
139
140 Plasma::DataEngine *m_timeEngine = dataEngine("time");
141 m_timeEngine->connectSource( "Local", this, 6000, Plasma::AlignToMinute);
142
143 connect(m_map, SIGNAL(repaintNeeded(QRegion)), this, SLOT(slotRepaint()));
144}
145
146WorldClock::~WorldClock()
147{
148 delete m_map;
149}
150
151void WorldClock::resizeMap(bool changeAspect)
152{
153 int width = 0;
154 int height = 0;
155 int radius = 0;
156 double ratio = static_cast<double>(m_lastRect.width()) /
157 static_cast<double>(m_lastRect.height());
158 if( m_map->projection() == Equirectangular ) {
159 kDebug() << "equirectangular with rect" << m_lastRect;
160 kDebug() << "w/h ratio:" << ratio;
161 if( ratio > 2 ) {
162 height = m_lastRect.height();
163 width = height*2;
164 radius = static_cast<int>(height/2);
165 } else {
166 width = m_lastRect.width();
167 height = static_cast<int>(width/2);
168 radius = static_cast<int>(width/4);
169 }
170 } else if( m_map->projection() == Mercator ) {
171 kDebug() << "mercator with rect" << m_lastRect;
172 kDebug() << "w/h ratio:" << ratio;
173 if( ratio > 1 ) {
174 height = m_lastRect.height();
175 width = height;
176 radius = static_cast<int>(width/4);
177 } else {
178 width = m_lastRect.width();
179 height = width;
180 radius = static_cast<int>(width/4);
181 }
182 }
183 kDebug() << "width, height, radius:" << width << height << radius;
184
185 m_map->setSize(width, height);
186 m_map->setRadius( radius );
187 update();
188 if(changeAspect) {
189 QRectF curGeo = geometry();
190 setGeometry( curGeo.x(), curGeo.y(), static_cast<double>(width),
191 static_cast<double>(height) );
192 }
193}
194
195void WorldClock::slotRepaint()
196{
197 update();
198}
199
200void WorldClock::dataUpdated(const QString &source,
201 const Plasma::DataEngine::Data &data)
202{
203 Q_UNUSED(source)
204 //kDebug() << "Time = " << data["Time"].toTime();
205 m_localtime = QDateTime( QDate::currentDate(), data["Time"].toTime() );
206 m_time = KSystemTimeZones::local().convert(m_locations.value(m_locationkey),
207 m_localtime );
208 //kDebug() << "Adjusted Time = " << m_time;
209 update();
210}
211
212void WorldClock::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
213{
214 m_isHovered = false;
215 Applet::hoverLeaveEvent(event);
216 update();
217}
218void WorldClock::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
219{
220 m_isHovered = true;
221 m_hover = event->pos() - m_t;
222 Applet::hoverEnterEvent(event);
223 setTz( getZone() );
224 update();
225}
226void WorldClock::hoverMoveEvent(QGraphicsSceneHoverEvent *event)
227{
228 m_hover = event->pos() - m_t;
229 Applet::hoverMoveEvent(event);
230 setTz( getZone() );
231 update();
232}
233
234QString WorldClock::getZone()
235{
236 qreal lat, lon;
237 // get the hover zone only if the hove point exists
238 bool ok = !m_hover.isNull() &&
239 m_map->viewport()->geoCoordinates( m_hover.x(), m_hover.y(), lon, lat );
240
241 if( !ok ) {
242 return KSystemTimeZones::local().name();
243 }
244 QList<QString> zones = m_locations.keys();
245
246 QString closest;
247 qreal mindist = 99999999999999999.9;
248
249 for (int i = 0; i < zones.size(); ++i ) {
250 KTimeZone cz = m_locations.value( zones.at( i ) );
251 qreal dist = sqrt( pow(lat-cz.latitude(), 2) + pow(lon-cz.longitude(), 2) );
252 if ( dist < mindist ) {
253 mindist = dist;
254 closest = zones.at( i );
255 }
256 }
257 return m_locations.value( closest ).name();
258}
259
260void WorldClock::setTz( QString newtz )
261{
262 if ( newtz == m_locationkey ) { return; }
263 m_locationkey = newtz;
264 m_time = KSystemTimeZones::local().convert(m_locations.value(m_locationkey),
265 m_localtime );
266 recalculateFonts();
267}
268
269void WorldClock::recalculatePoints()
270{
271 int x = m_map->width();
272 int y = m_map->height();
273 m_points.insert( "topright", QPoint( ( x*0.666 ), ( y*0.25 ) ) );
274 m_points.insert( "topleft", QPoint( ( x*0.333 ), ( y*0.25 ) ) );
275 m_points.insert( "middleright", QPoint( ( x*0.666 ), ( y*0.58333 ) ) );
276 m_points.insert( "middleleft", QPoint( ( x*0.333 ), ( y*0.58333 ) ) );
277 m_points.insert( "bottomright", QPoint( ( x*0.666 ), ( y*0.75 ) ) );
278 m_points.insert( "bottomleft", QPoint( ( x*0.333 ), ( y*0.75 ) ) );
279 return;
280
281}
282
283void WorldClock::recalculateFonts( )
284{
285 QString timestr;
286 if(m_showDate)
287 timestr = KGlobal::locale()->formatDateTime( m_time );
288 else
289 timestr = KGlobal::locale()->formatTime( m_time.time() );
290
291 QString locstr = i18n( m_locationkey.toUtf8().data() );
292 locstr.remove( 0, locstr.lastIndexOf( '/' ) + 1 ).replace( '_', ' ' );
293 QRect timeRect( m_points.value( "topleft" ), m_points.value( "middleright" ) );
294 QRect locationRect( m_points.value( "middleleft" ), m_points.value( "bottomright" ) );
295
296 m_locationFont = calculateFont(locstr, locationRect);
297 m_timeFont = calculateFont(timestr, timeRect);
298}
299
300QFont WorldClock::calculateFont(const QString &text, const QRect &boundingBox)
301{
302 QFont resultFont( "Helvetica", 3, QFont::Bold);
303
304 int unscaled = 0; // Avoid infinite loops, bug 189633
305 QRect lastBox;
306
307 //we set very small defaults and then increase them
308 for ( int curSize = resultFont.pointSize()+1; unscaled<100; ++curSize ) {
309 resultFont.setPointSize(curSize);
310 QFontMetrics metrics( resultFont );
311 QRect rect = metrics.boundingRect( text );
312 if ( rect.width() > boundingBox.width() ||
313 rect.height() > boundingBox.height() ) {
314 break;
315 }
316
317 if ( rect.width() > lastBox.width() ||
318 rect.height() > lastBox.height() ) {
319 unscaled = 0;
320 }
321 else {
322 ++unscaled;
323 }
324
325 lastBox = rect;
326 }
327
328 resultFont.setPointSize(resultFont.pointSize()-1);
329 return resultFont;
330}
331
332void WorldClock::recalculateTranslation()
333{
334 m_t = QPoint(static_cast<int>( (m_lastRect.width()/2) - (m_map->width()/2) ),
335 static_cast<int>( (m_lastRect.height()/2) - (m_map->height()/2) ));
336 m_t += m_lastRect.topLeft();
337}
338
339void WorldClock::paintInterface(QPainter *p,
340 const QStyleOptionGraphicsItem *option,
341 const QRect &contentsRect)
342{
343 Q_UNUSED(option)
344 if ( contentsRect != m_lastRect ) {
345 m_lastRect = contentsRect;
346 resizeMap();
347 recalculateTranslation();
348 recalculatePoints();
349 recalculateFonts();
350 }
351 p->setRenderHint( QPainter::TextAntialiasing , true );
352 p->setRenderHint( QPainter::Antialiasing , true );
353 p->setPen( Qt::NoPen );
354 //p->setBrush( QBrush( QColor( 0x00, 0x00, 0x00, 0xFF ) ) );
355 //p->drawRect( m_lastRect );
356 QPixmap pixmap( m_map->width(), m_map->height() );
357 pixmap.fill( Qt::transparent );
358 GeoPainter gp( &pixmap, m_map->viewport(),
359 Marble::NormalQuality );
360 QRect mapRect( 0, 0, m_map->width(), m_map->height() );
361 m_map->paint(gp, mapRect );
362 p->drawPixmap( m_t, pixmap );
363
364 if ( !m_isHovered ) {
365 setTz( KSystemTimeZones::local().name() );
366 }
367
368 //Show the location on the map
369 qreal tzx = 0;
370 qreal tzy = 0;
371 qreal lon = m_locations.value(m_locationkey).longitude() * DEG2RAD;
372 qreal lat = m_locations.value(m_locationkey).latitude() * DEG2RAD;
373 bool ok = m_map->viewport()->screenCoordinates(lon, lat, tzx, tzy);
374 if ( ok /*&& m_isHovered*/ ) {
375 QPoint tz( tzx, tzy );
376 tz += m_t;
377 int radius = m_lastRect.width() / 40;
378 QRadialGradient grad( tz, radius );
379 grad.setColorAt( 0, QColor( 0xFF, 0xFF, 0x00, 0xFF ) );
380 grad.setColorAt( 0.33, QColor( 0xFF, 0xFF, 0x00, 0x46 ) );
381 grad.setColorAt( 0.66, QColor( 0xFF, 0xFF, 0x00, 0x14 ) );
382 grad.setColorAt( 1, QColor( 0xFF, 0xFF, 0x00, 0x00 ) );
383 p->setBrush( QBrush( grad ) );
384 p->drawEllipse( tz, radius, radius );
385 }
386
387 p->setPen( QColor( 0xFF, 0xFF, 0xFF ) );
388
389 QString locstr = i18n( m_locationkey.toUtf8().data() );
390 locstr.remove( 0, locstr.lastIndexOf( '/' ) + 1 ).replace( '_', ' ' );
391
392 QString timestr;
393 if(m_showDate)
394 timestr = KGlobal::locale()->formatDateTime( m_time );
395 else
396 timestr = KGlobal::locale()->formatTime( m_time.time() );
397
398 p->setFont( m_timeFont );
399 p->drawText( QRect( m_points.value( "topleft" ) + m_t,
400 m_points.value( "middleright" ) + m_t ),
401 Qt::AlignCenter, timestr );
402
403 p->setFont( m_locationFont );
404 p->drawText( QRect( m_points.value( "middleleft" ) + m_t,
405 m_points.value( "bottomright" ) + m_t ),
406 Qt::AlignCenter, locstr );
407}
408
409void WorldClock::createConfigurationInterface(KConfigDialog *parent)
410{
411 QWidget *widget = new QWidget();
412 ui.setupUi(widget);
413 parent->setButtons(KDialog::Ok | KDialog::Apply | KDialog::Cancel);
414
415 KConfigGroup cg = config();
416
417 ui.longitudeEdit->setValue(cg.readEntry("rotation", -20));
418
419 if(cg.readEntry("projection", static_cast<int>(Equirectangular)) == Mercator)
420 ui.projection->setCurrentIndex(1);
421 else //Equirectangular is the default projection
422 ui.projection->setCurrentIndex(0);
423
424 if(cg.readEntry("daylight", false ))
425 ui.daylightButton->setChecked(true);
426
427 if(cg.readEntry("showdate", false ))
428 ui.showdate->setChecked(true);
429
430 if(cg.readEntry("customtz", false ))
431 ui.customTz->setChecked(true);
432
433 ui.tzWidget->setSelectionMode( QTreeView::MultiSelection );
434 foreach(const QString& tz, cg.readEntry("tzlist")) {
435 ui.tzWidget->setSelected(tz,true);
436 }
437
438 connect(parent, SIGNAL(okClicked()), this, SLOT(configAccepted()));
439 connect(parent, SIGNAL(applyClicked()), this, SLOT(configAccepted()));
440 parent->addPage(widget, parent->windowTitle(), icon());
441}
442
443void WorldClock::configAccepted()
444{
445 KConfigGroup cg = config();
446
447 if( ui.daylightButton->isChecked() )
448 m_map->setSubSolarPointIconVisible(true);
449 else {
450 m_map->centerOn(ui.longitudeEdit->value(), 0);
451 update();
452 }
453
454 m_showDate = ui.showdate->isChecked();
455 m_customTz = ui.customTz->isChecked();
456
457 if(m_customTz) {
458 QStringList tzlist = ui.tzWidget->selection();
459 kDebug() << "\tSetting TZLIST";
460 kDebug() << tzlist;
461 QMap<QString, KTimeZone> selectedZones;
462 selectedZones.insert(KSystemTimeZones::local().name(),
463 KSystemTimeZones::local());
464 foreach( const QString& tzname, tzlist ) {
465 selectedZones.insert(tzname, KSystemTimeZones::zone(tzname));
466 }
467 cg.writeEntry("tzlist",tzlist);
468 m_locations = selectedZones;
469 if(!m_locations.contains(m_locationkey))
470 m_locationkey = m_locations.keys().first();
471 }
472
473 // What projection? note: +1 because the spherical projection is 0
474 if((ui.projection->currentIndex() + 1) != cg.readEntry("projection",
475 static_cast<int>(Equirectangular)) )
476 {
477 switch ( ui.projection->currentIndex() ) {
478 case 1:
479 //kDebug() << "case 1, setting proj to mercator";
480 m_map->setProjection(Mercator);
481 update();
482 resizeMap(true);
483 cg.writeEntry("projection", static_cast<int>(Mercator));
484 break;
485 //case 0 (and anything else that pops up)
486 default:
487 //kDebug() << "case default, setting proj to Equirectangular";
488 m_map->setProjection(Equirectangular);
489 update();
490 resizeMap(true);
491 cg.writeEntry("projection", static_cast<int>(Equirectangular));
492 break;
493 }
494 }
495
496 cg.writeEntry("rotation", ui.longitudeEdit->value());
497 cg.writeEntry("centersun", ui.daylightButton->isChecked());
498 cg.writeEntry("showdate", ui.showdate->isChecked());
499 cg.writeEntry("customtz", ui.customTz->isChecked());
500
501 emit configNeedsSaving();
502}
503
504} //ns Marble
505
506#include "worldclock.moc"
507