Let's continue by implementing the weather forecast view. This view will be entirely composed of non-foldable ContentPanel controls. We'll have one panel that displays the city for which the forecast is for and one panel for each day of the five-day forecast. In order to make it easy to change the number of days the forecast is for we'll store references to the forecast panels in an array. Thus, we need to add two global variables to the widget:
// Weather view controls. var weatherCityPanel; var weatherContentPanels;
The next step is to create the actual view in the createWeatherView() function. Of course the actual content for the content panels won't be known until at runtime when we know what city the forecast is for and what the actual weather forecast is. Still, we can create the actual content panels and just set their content later so this is not a problem.
// Creates the weather view. function createWeatherView() { // empty caption text to display the caption bar - custom background using CSS weatherView = new ListView(null, ""); // heading panel for city weatherCityPanel = new ContentPanel(); weatherView.addControl(weatherCityPanel); // create five content panels - one for each day in the 5-day forecast weatherContentPanels = []; for (var i = 0; i < 5; i++) { var weatherContentPanel = new ContentPanel(); weatherView.addControl(weatherContentPanel) weatherContentPanels.push(weatherContentPanel); } }
When we show the weather view we have to check what the current local city is and set the weatherCityPanel's content to a suitable HTML fragment that will work as a heading for the weather forecast. In addition to that we'll have to retrieve the weather forecast from the business logic engine and then create HTML from that for each day in the forecast and set it to the five content panels that we are storing references to in the weatherContentPanels array. The caption for the weather forecast panels will be a string that indicates the day that the forecast is for.
Creating the HTML fragment for the forecast heading doesn't sound so hard but generating HTML for a weather forecast for five days sounds a bit tricky. Let's create a separate function to do that. Remember that the weather object that the engine is returning to us has two properties: temperature and type. The temperature is in the preferred temperature unit and the type is one of the type codes, e.g. "Sunny". We'll use it to match it up with a weather icon.
// Returns HTML for one day of weather forecast. function getHTMLForWeather(weather) { // build weather icon file name string var weatherIcon = "Weather" + weather.type + ".png"; // build temperature string var temperatureStr = weather.temperature + "°" + engine.getTemperatureUnit().toUpperCase(); // build weather HTML var weatherBuf = ""; weatherBuf += "<table class=\"WeatherForecastDayTable\"><tr>"; weatherBuf += "<td class=\"WeatherForecastIconCell\">"; weatherBuf += "<img src=\"" + weatherIcon + "\"/>"; weatherBuf += "</td>"; weatherBuf += "<td class=\"WeatherForecastTemperatureCell\">"; weatherBuf += temperatureStr; weatherBuf += "</td>"; weatherBuf += "</tr></table>"; return weatherBuf; }
The HTML that we are generating is a table with a single row with two cells. The left cell has the weather icon image and the right cell has the temperature. We are using three CSS style rules for the table and we naturally need to create those too in the TravelCompanion.css file:
/* Table for one day of weather forecast information */ .WeatherForecastDayTable { margin: auto; border-spacing: 0px; } /* Table cell for weather icon */ .WeatherForecastIconCell { line-height: 1px; font-size: 1px; vertical-align: middle; } /* Table cell for temperature information */ .WeatherForecastTemperatureCell { padding: 0px 0px 0px 10px; vertical-align: middle; }
Now we have a way to turn a weather forecast into HTML that we can use in a ContentPanel control. We'll do that in a function we call updateWeatherForecast(). The caption for each weather forecast panel will indicate the day that the forecast is for. The first weather object in the array that the engine returns is for today and the subsequent ones are for coming days.
// Updates the weather forecast. function updateWeatherForecast() { // get local time and weather var localTime = engine.getLocalTime(); var localWeather = engine.getLocalWeather(); // set the weather for each day in the forecast for (var i = 0; i < 5; i++) { // figure out day name var day = (localTime.day + i) % 7; var dayName = localTime.dayNames[day]; // set weather to content panel weatherContentPanels[i].setCaption((i == 0 ? "Today, " : "") + dayName); weatherContentPanels[i].setContent(getHTMLForWeather(localWeather[i])); } }
We are now ready to implement the showWeatherView() function:
// Displays the weather view. function showWeatherView() { // set heading city panel weatherCityPanel.setContent("<div class=\"WeatherCityPanel\">" + engine.getLocalCity().name + " 5-day Forecast("</div>"); // update the weather forecast before showing the view updateWeatherForecast(); setSubViewSoftkeys(); uiManager.setView(weatherView); }
The weatherCityPanel gets a heading that matches the currently configured local city. We're using another CSS rule for that so we'll have to add it to our stylesheet:
/* City heading panel for weather forecast view */ .WeatherCityPanel { font-size: 14px; font-weight: bold; padding: 0px 0px 10px 0px; }
The weather forecast view and functionality is now ready to be tested.
It's time to move on to the currency converter. This view is going to have two TextField controls. One for home currency and one for local currency. In addition there will be two FormButton controls to convert from the home currency to the local currency, and vice versa. We'll need references to the textfields so that we can retrieve their values when the form buttons are pressed:
// Converter view controls. var homeMoneyField; var localMoneyField;
Creating the view is quite straight forward:
// Creates the converter view. function createConverterView() { // empty caption text to display the caption bar - custom background using CSS converterView = new ListView(null, ""); // home money homeMoneyField = new TextField(); converterView.addControl(homeMoneyField); // local money localMoneyField = new TextField(); converterView.addControl(localMoneyField); // home to local var homeToLocalButton = new FormButton(null, "Convert Home to Local"); converterView.addControl(homeToLocalButton); // local to home var localToHomeButton = new FormButton(null, "Convert Local to Home"); converterView.addControl(localToHomeButton); }
Notice that the textfields don't have any caption or text at this point. We will set that just before the view is displayed because the caption will depend on what the home and local cities are configured to.
Also notice that we don't have any event listeners for the buttons at this point. That's because we haven't actually written any functions that will implement the currency conversion yet. But before we do that we'll implement the function to show the view:
// Displays the converter view. function showConverterView() { // set captions and reset fields homeMoneyField.setCaption("Home Currency (" + engine.getHomeCity().currency + ")"); homeMoneyField.setText(""); localMoneyField.setCaption("Local Currency (" + engine.getLocalCity().currency + ")"); localMoneyField.setText(""); setSubViewSoftkeys(); uiManager.setView(converterView); }
You can now test the view but it won't actually convert any currency until we give our form buttons event listeners and write the code that will be called when the buttons are pressed.
We'll implement that in two functions. One to convert from home to local and another to convert in the other direction. The functions take the current text value from the corresponding field and then pass it to the engine to do the conversion. The result is then placed in the other of the two fields, formatted so that the result has exactly two digits.
// Called when the user clicks on the "convert home to local" button // in the converter view. (rounds to two decimals) function convertHomeToLocalMoney() { var homeMoney = parseFloat(homeMoneyField.getText()); var localMoney = engine.convertHomeToLocalMoney(homeMoney).toFixed(2); localMoneyField.setText(localMoney); } // Called when the user clicks on the "convert local to home" button // in the converter view. (rounds to two decimals) function convertLocalToHomeMoney() { var localMoney = parseFloat(localMoneyField.getText()); var homeMoney = engine.convertLocalToHomeMoney(localMoney).toFixed(2); homeMoneyField.setText(homeMoney); }
Now that the functions are implemented we can add the event listeners to our form buttons. We'll add this code right after where the buttons are created in the createConverterView() function:
homeToLocalButton.addEventListener("ActionPerformed", convertHomeToLocalMoney); localToHomeButton.addEventListener("ActionPerformed", convertLocalToHomeMoney);
The currency converter is now working and it's time to test it. Try it with different currencies by changing the home and local cities in the settings view.
The information view is a summary of relevant information that the user is will often need. Here we'll have a world clock that shows the home and local time, the current weather at home and in the local city, as well as the latest news headlines. We will use ContentPanel controls for all of these. One for the home time, one for the local time, one for the home weather, one for the local weather, and one for all the news headlines.
We'll need to access all of these content panels from outside the view creation function so we'll need global variables to hold references to them:
// Info view controls. var homeCityTimePanel; var localCityTimePanel; var homeCityWeatherPanel; var localCityWeatherPanel; var newsHeadlinesPanel;
We can now proceed and actually implement the creation of the information view:
// Creates the info view. function createInfoView() { // empty caption text to display the caption bar - custom background using CSS infoView = new ListView(null, ""); // home city time homeCityTimePanel = new ContentPanel(); infoView.addControl(homeCityTimePanel); // local city time localCityTimePanel = new ContentPanel(); infoView.addControl(localCityTimePanel); // separator infoView.addControl(new Separator()); // home city weather homeCityWeatherPanel = new ContentPanel(); infoView.addControl(homeCityWeatherPanel); // local city weather localCityWeatherPanel = new ContentPanel(); infoView.addControl(localCityWeatherPanel); // separator infoView.addControl(new Separator()); // news headlines newsHeadlinesPanel = new ContentPanel(); infoView.addControl(newsHeadlinesPanel); }
We already have a function that turns a weather forecast into HTML but we don't have similar functions for time or for news headlines. We'll need that when we want to put content into the time and headline content panels so let's write those functions next.
For time we'll need a function that takes a DateTime object and returns HTML that we can use in the content panel in the information view.
// Returns HTML for time. function getHTMLForTime(time) { // build HTML buffer var timeBuf = ""; timeBuf += "<div class=\"Clock\">"; timeBuf += time.hours + ":" + (time.minutes < 10 ? "0" : "") + time.minutes; timeBuf += "</div>"; return timeBuf; }
The HTML uses a CSS rule that we'll have to define in our stylesheet:
/* Clock div */ .Clock { text-align: center; font-size: 16px; font-weight: bold; }
Next up is the function for turning the latest news headlines into HTML. For each news headline we'll generate a div that contains the actual news headline plus a link to the website where the full news article is. Opening URLs should be done using the widget.openURL() method in the S60 Web Runtime but since we don't have that functionality in a PC browser we'll write a wrapper for the function just like we did for the RSS Reader:
// Opens a URL. function openURL(url) { if (window.widget) { // in WRT widget.openURL(url); } else { // outside WRT window.open(url, "NewWindow"); } }
Now we can implement the function that turns news headlines into HTML:
// Returns HTML for news headlines. function getHTMLForNewsHeadlines(newsHeadlines) { var newsBuf = ""; for (var i = 0; i < newsHeadlines.length; i++) { newsBuf += "<div class=\"NewsHeadline\">"; newsBuf += newsHeadlines[i].headline + "<br/>"; newsBuf += "<a href=\"JavaScript:openURL('" + newsHeadlines[i].url + "');\">"; newsBuf += "Read more..."; newsBuf += "</a>"; newsBuf += "</div>"; } return newsBuf; }
The div that encloses each news headline uses a CSS rule called NewsHeadline. As with all other CSS rules that we use we'll define this one too in the TravelCompanion.css stylesheet file. We also need to add rules for what links should look like in the context of a news headline. We define the link as bold underlined blue text in its normal state and inverse with blue background and white text when focused.
/* News headline */ .NewsHeadline { padding: 0px 0px 10px 0px; } /* Anchor tags in the context of a news headline link */ .NewsHeadline a { text-decoration: underline; font-weight: bold; color: rgb(0,0,255); } /* Focused anchor tags */ .NewsHeadline a:focus { background: rgb(0,0,255); color: rgb(255,255,255); }
Now we should have everything we need to be able to update the information view content panels when the view is shown. It's time to implement the showInfoView() function:
// Displays the info view. function showInfoView() { // set current information to controls var homeCity = engine.getHomeCity(); var localCity = engine.getLocalCity(); // set time var homeTime = engine.getHomeTime(); var localTime = engine.getLocalTime(); homeCityTimePanel.setCaption(homeCity.name + " Time"); homeCityTimePanel.setContent(getHTMLForTime(homeTime)); localCityTimePanel.setCaption(localCity.name + " Time"); localCityTimePanel.setContent(getHTMLForTime(localTime)); // set weather var homeWeather = engine.getHomeWeather(); var localWeather = engine.getLocalWeather(); homeCityWeatherPanel.setCaption(homeCity.name + " Weather"); homeCityWeatherPanel.setContent(getHTMLForWeather(homeWeather[0])); localCityWeatherPanel.setCaption(localCity.name + " Weather"); localCityWeatherPanel.setContent(getHTMLForWeather(localWeather[0])); // set headline var newsHeadlines = engine.getNewsHeadlines(); newsHeadlinesPanel.setCaption("News Headlines"); newsHeadlinesPanel.setContent(getHTMLForNewsHeadlines(newsHeadlines)); setSubViewSoftkeys(); uiManager.setView(infoView); }
If you try the view now, everything will seem to work fine. But there is a big problem! We set the content in the content panels when the view is shown but if we stay in the view for an hour the clocks will still show the same time as when we showed the view. What's worse, it's not just the information view that has this problem. The weather forecast view is equally broken.
In order to keep the views up to date we'll need to start a timer that periodically calls a function where we examine whether it's been long enough since the last update of a view that it's time to update the content in that view. Note that we only need to update a view using the timer if the view is actually visible.
That sounds good at first but some things have to be updated more frequently than others. The clocks in our information view have to not only be updated once a minute but immediately when the minute changes. Such accuracy is not needed for the weather forecasts or news headlines. For them it's fine if they are updated once per hour. Except that weather forecasts also have to be updated when the day rolls over to the next as otherwise the forecast will show the wrong day. Because the home and local cities can be in different timezones their days will roll over at different times and thus we have to track these separately.
It turns out that we need no less than five variables to track this. We need one to track the last minute that has been updated to the clocks, one variable is needed to track when the weather was last updated, one to track the day in the home city when the home city weather forecast was updated, one to track the same but for the local city, and one to track when the news headlines were updated. Let's declare those variables and initialize them:
// Tracks last updated minute on the clocks. var lastUpdatedClockMinute = -1; // Tracks last update time for weather. var weatherLastUpdated = -1; // Tracks the days when the weather was last updated. var lastUpdatedHomeWeatherDay = -1; var lastUpdatedLocalWeatherDay = -1; // Tracks last update time for news headlines. var newsHeadlinesLastUpdated = -1;
We'll use the time in millieconds to remember update times and compare how long it's been since an update happened. Since we'll use this quite a lot, let's create a small helper function for it:
// Returns the current time in milliseconds. function getTimeMillis() { return new Date().getTime(); }
When we show a view we have to update the appropriate tracking variables so the widget will properly keep track of when a view was updated. That means that we have to add the following to just before we show the information view in showInfoView():
// update the tracking variables lastUpdatedClockMinute = homeTime.minutes; weatherLastUpdated = getTimeMillis(); lastUpdatedHomeWeatherDay = homeTime.day; lastUpdatedLocalWeatherDay = localTime.day; newsHeadlinesLastUpdated = getTimeMillis();
In the same way the showWeatherView() function needs the following addition:
// update the tracking variables weatherLastUpdated = getTimeMillis(); lastUpdatedLocalWeatherDay = engine.getLocalTime().day;
Now our tracking variables should be updated to the right times whenever views are shown but we still don't have a timer to handle the automatic updating of the views.
We'll set this timer up so that the timer callback function is called once every second so that we can make sure that the clocks don't fall behind. We'll track the timer identifier in a variable:
// View updating timer identifier. var timerId;
We'll make the timer call a function called updateViews() that we will create shortly but first let's start the timer as the last thing in the init() function:
// start timer that keeps views up to date timerId = setInterval(updateViews, 1000);
The updateViews() function needs to check what view we are currently in and do the appropriate checks depending on the view. If we're in the information view then we have three things to check. The first thing is to check if the minute has changed on the clock. The second is to check if it's been over 60 minutes since the weather was updated or if either the home or local city has rolled over to the next day. And finally the third to thing to check is if it's been over 60 minutes since the news headlines were updated.
If we are in the weather view then we check if it's been over 60 minutes since the weather was updated or if the local city has rolled over to the next day. Note that the weather forecast view only displays the local weather forecast so we don't have to check for if the home city has rolled over to the next day.
The implementation of the function is therefore as follows:
// Timer callback function that gets called once every second to keep views up to date. function updateViews() { // get the current view var currentView = uiManager.getView(); // get home and local time as well as time in milliseconds var homeTime = engine.getHomeTime(); var localTime = engine.getLocalTime(); var now = getTimeMillis(); if (currentView == infoView) { // only update the clocks if the minute has changed if (homeTime.minutes != lastUpdatedClockMinute) { lastUpdatedClockMinute = homeTime.minute; homeCityTimePanel.setContent(getHTMLForTime(homeTime)); localCityTimePanel.setContent(getHTMLForTime(localTime)); } // update weather if it hasn't been updated in the last hour or if the day has changed if ((now > weatherLastUpdated + (1000 * 60 * 60)) || (homeTime.day != lastUpdatedHomeWeatherDay) || (localTime.day != lastUpdatedLocalWeatherDay)) { weatherLastUpdated = now; lastUpdatedHomeWeatherDay = homeTime.day; lastUpdatedLocalWeatherDay = localTime.day; var homeWeather = engine.getHomeWeather(); var localWeather = engine.getLocalWeather(); homeCityWeatherPanel.setContent(getHTMLForWeather(homeWeather[0])); localCityWeatherPanel.setContent(getHTMLForWeather(localWeather[0])); } // update news headlines if they haven't been updated in the last hour if (now > newsHeadlinesLastUpdated + (1000 * 60 * 60)) { newsHeadlinesLastUpdated = now; var newsHeadlines = engine.getNewsHeadlines(); newsHeadlinesPanel.setContent(getHTMLForNewsHeadlines(newsHeadlines)); } } else if (currentView == weatherView) { // update weather if it hasn't been updated in the last hour or if the day has changed if ((now > weatherLastUpdated + (1000 * 60 * 60)) || (localTime.day != lastUpdatedLocalWeatherDay)) { weatherLastUpdated = now; lastUpdatedLocalWeatherDay = localTime.day; updateWeatherForecast(); } } }
The Travel Companion widget is now completed! We have learned to implement complex widgets with multiple views using the WRTKit. We have also learned how to keep information in views up to date using a timer. And we implemented the widget without any dependencies to how the business logic engine was implemented. That will come in handy if you want to continue developing this widget by substituting the mock engine with one that actually uses real world data. You are now ready to tackle any challenge using the WRTKit!