‘Twas the night before Christmas, and all through the house…
For millions of people worldwide, Christmas eve is a time of magic. Children are excited and unable to sleep because…
Santa is coming tonight!!!!
This is the one night of the year you want a stranger to come into your house whilst you sleep. He’ll magic his way in (cos who has a chimney these days, right?), and leave presents under your tree.
He know if you are sleeping, He knows if you’re awake…
But - and this is important, he only comes if you are asleep. So children, you need to be asleep early. But how early? How do you know when you need to be asleep by?
For this - you need a Santa tracker mobile app. And not just any Santa tracker, a Fabulous one!
This app is pretty simple. It knows where Santa is at any given time on Christmas eve, and based on the current time displays the location of Santa on a map, along with a running count of how many presents he’s delivered so far to all the good girls and boys.
The code is all on GitHub if you want to check it out.
Building the Santa tracker
To build this app I needed a source of data, and a mobile app.
Where did the data come from?
Every year for the past decade or so, NORAD has been tracking Santa online every Christmas eve. Unfortunately they don’t seem to have a public API that I could find to use. Luckily Google has got in on the action, and provides an undocumented API to download a JSON file containing Santas coordinates from last year. I simply downloaded that JSON document to use. The structure is pretty simple, and I’ve shown a snippet of it below showing only the bits I’m interested in.
{
"destinations": [
{
"arrival": 31536000000,
"presentsDelivered": 0,
"city": "Santa's Village",
"region": "North Pole",
"location": {
"lat": 84.6,
"lng": 168
}
},
{
"arrival": 1514110140000,
"presentsDelivered": 46415,
"city": "Provideniya",
"region": "Russia",
"location": {
"lat": 64.436249,
"lng": -173.233337
}
...
]
}
This gives a city and region name for each stop, total presents delivered, the latitude and longitude of the location and the arrival time. This arrival time is the number of milliseconds since the UNIX epoch - 1 Jan 1970.
Creating the mobile app
For the mobile app, I wanted to use F#, and there is a great F# mobile framework called Fabulous!
Fabulous?
If you haven’t heard of Fabulous yet, its an F#-based MVU framework (so like Elm) for building cross-platform mobile apps, built on top of the Xamarin.Forms platform. You can read all about it in the docs. There’s even an awesome-fabulous list containing cool projects that use it and some great resources.
You can create a new Fabulous app using the dotnet
CLI. You install the template using:
dotnet new -i Fabulous.Templates
The create the app using:
dotnet new fabulous-app -n FabulousSantaTracker
This will create an F# solution with an Android app, an iOS app and a .NET standard library containing all the cross platform business logic and UI code.
Reading the JSON
Processing the JSON file is easy. First I added the downloaded JSON file to the core project, setting the build action to EmbeddedResource
. Once I had this, I added a new F# source file and declared some new types to store the data.
type Location = { Lat : float; Lng : float }
type Destination =
{
Arrival : float
PresentsDelivered : int64
City : string
Region : string
Location : Location
}
type Destinations = array<Destination>
type SantaData = { Destinations : Destinations }
To read the JSON data I needed to read the embedded resource, then use Newtonsoft.JSON to deserialize it (the NuGet for this is included in the Fabulous template, so nothing extra to install).
let GetResourceString fileName =
let assembly = IntrospectionExtensions.GetTypeInfo(typedefof<Destination>).Assembly
use stream = assembly.GetManifestResourceStream(fileName)
use reader = new StreamReader (stream)
reader.ReadToEnd()
let AllDestinations =
let santaData = JsonConvert.DeserializeObject<SantaData>(GetResourceString "FabulousSantaTracker.santa_en.json")
santaData.Destinations
The GetResourceString
function loads the assembly and extracts the resource string from it. The name of these resource strings needs to be namespace qualified. For example the file in my project is called santa_en.json
, and my namespace is FabulousSantaTracker
, so the full resource name is FabulousSantaTracker.santa_en.json
.
The last bit I needed was to convert the arrival date from milliseconds after epoch. These arrival dates are for last year (2017), so I needed to update them to the current year.
let convertFromEpoch ms =
let d = epoch.AddMilliseconds (ms)
d.AddYears(DateTime.UtcNow.Year - d.Year)
Now I have my data, it’s time to display this on a map. Fabulous uses an MVU architecture, so I need to define a model, some messages, an update function to process these messages, and a view function.
Defining the model
The model is simple - it just needs Santa’s current location by finding the latest destination in the list with an arrival time before now by working backwards through the list of destinations loaded from the JSON file. If Santa hasn’t arrived anywhere yet then the first destination is used - this is Santa’s workshop at the North Pole.
let currentDestination () =
let current = TrackingData.AllDestinations |> Array.tryFindBack (fun i -> i.ArrivalDateTime < DateTime.UtcNow)
match current with
| Some d -> d
| None -> TrackingData.AllDestinations |> Array.item 0
type Model =
{
CurrentDestination : Destination
}
let init () = { CurrentDestination = currentDestination() }, Cmd.none
Updating the map when Santa moves
The easiest way to update the current destination when Santa moves is a simple timer - fire a timer every few seconds and update the current destination. This can be implemented by defining a message for a timer tick, and when this is handled in the update
function, update the models CurrentDestination
on every tick.
type Msg =
| TimerTick
let update msg model =
match msg with
| TimerTick -> { model with CurrentDestination = currentDestination() }, Cmd.none
This message can then be fired using a subscription to a timer. These subscriptions are functions that take the Fabulous dispatcher and are called when setting up the program.
let timerTick dispatch =
let timer = new Timer(TimeSpan.FromSeconds(10.).TotalMilliseconds)
timer.Elapsed.Subscribe (fun _ -> dispatch TimerTick) |> ignore
timer.Enabled <- true
timer.Start()
let runner =
App.program
|> Program.withSubscription (fun _ -> Cmd.ofSub App.timerTick)
Once this subscription is called, the timer is started and every 10 seconds the TimerTick
message is dispatched. This causes the models CurrentDestination
to be re-evaluated and the view then gets updated.
Drawing the view
Drawing the view is pretty simple. Fabulous uses a virtual UI, so your view
function always returns a complete UI, and the internals of Fabulous compare the current with the result of the view
call and applies the deltas to the real UI.
My view starts with a navigation page containing a content page with some emoji in the title:
let view (model: Model) dispatch =
View.NavigationPage(
pages = [
View.ContentPage(
title = "🎅 Tracker"
...
]
It then contains a grid with 3 rows. First row for the current location, second for the number of presents, third is a map.
The label to show the location has the text set like this:
View.Label(text = model.CurrentDestination.City,
This is one of the ways MVU differs from MVVM (the canonical design pattern for Xamarin.Forms apps) - the value for the location isn’t bound to anything, it’s just set using the value from the model. Updates to this value can only come from the update
function, and after updating, the view
function is called and the label is created with the new text. The differential update in the Fabulous framework will see this value change and update the UI.
To show the position on a map, we can use the Fabulous.Maps
NuGet package which contains a wrapper for the Xamarin.Forms Map element. These wrappers are the way that Fabulous can provide a virtual UI, so is needed for each component you want to use in your view
function. Once this NuGet is added, the map can be added to the view function.
View.Map(
hasZoomEnabled = true,
hasScrollEnabled = true,
requestedRegion = MapSpan.FromCenterAndRadius(model.CurrentDestination.Position, Distance.FromKilometers(1000.0)),
pins = [
View.Pin(
model.CurrentDestination.Position,
label = "Santa",
pinType = PinType.Place
)
]
)
This code creates a map, sets the region to the position of the current destination (the position is an instance of the Xamarin.Forms Position
class using the latitude and longitude from the JSON file) and zooms out to show a radius of 1,000Km. It will also add a pin at the position so that the user can see the position more accurately.
Using a custom map pin
This code so far shows where Santa is, and updates the UI when he moves, but doesn’t look the best as the pin is, well just a map pin. It would be better to show Santa himself.
To do this I added some images to the iOS and Android apps of Santa, drawn by using the Santa emoji. To show these as a custom pin means I need to dive down into some platform specific code. One of the upsides of Xamarin.Forms, the underlying technology for Fabulous, is that you have access to the native UI components, so can update them as if your app was a fully native swift/java app.
Accessing this native code involves the use of custom renderers - custom code to help Fabulous renderer the native controls. Customizing the map means creating my own SantaMap
class that derives from the Forms Map
class.
type SantaMap() =
inherit Xamarin.Forms.Maps.Map()
You can see the full implementation of this on GitHub. There is a lot of code here, but is lifted from the original maps implementation so was minimal work. Hopefully in the future this will be easier!
Once this was in place, I needed to create the custom renderers to draw different pins. The Android one is:
type SantaMapRenderer(context : Context) =
inherit MapRenderer(context)
override this.CreateMarker(pin : Pin) =
(new MarkerOptions()).SetPosition(new LatLng(pin.Position.Latitude, pin.Position.Longitude))
.SetTitle(pin.Label)
.SetIcon(BitmapDescriptorFactory.FromResource(Resources.Drawable.Santa))
module Export_SantaMapRenderer =
[<assembly: ExportRenderer(typeof<SantaMap>, typeof<SantaMapRenderer>) >]
do ()
Pretty simple - it uses the existing renderer and overrides the code to create the pins to use my Santa image. These renderers need to be registered with the framework to be used, and this can be done using the ExportRenderer
attributes.
Fin!
That’s all there is to this - a Santa tracker built using Fabulous. You can find all the code on GitHub here: https://github.com/jimbobbennett/FabulousSantaTracker
To build this app for iOS, just build it and run it and on Christmas eve watch for where Santa is. Android is a bit more effort as it needs a Google Maps key. You can find instructions on getting one in the Xamarin Docs. Once you have your key, add it to the AndroidManifest.xml
file and track Santa!
Merry Christmas!