TR

The Blog of Tyler Russell

An Angular2 Timezone Picker - Part 1: Becoming a Kartograph-er

TLDR

We design an SVG map to serve as the basis of our Angular2 timezone picker.

Using Kartograph, we generate a viable map candidate using shape file obtained from the tz_world database and Natural Earth Data.

We optimize the map using Kartograph configurations in order to cut down size and add attribution.

Introduction

In this multi-part series, we’ll be going through the process of building a timezone picker from scratch. We’ll talk through possible solutions and why we choose one route or the other.

Disclosure: I am not an Angular2 expert. At this point, I’m not sure anyone is. We’ll be learning together as we go through this process.

Disclosure 2: The first part of this series documents how to build the map for the component. Subsequent posts will document the process of building the Angular2 code. If you only care about Angular2, feel free to skip this and come back for the next.

The Goal

To keep this post as close to real life as possible, let assume we have no clue what we want to build (this has never happened to me, I promise). So let’s just find a reliable implementation to copy. The OSX timezone picker should work.

alt text

I fully acknowledge that I do not own copyrights to this picker, so we will get close to this, but not too close. For the sake of simplicity we won’t get into a full design spec. But here is a short list of what we are aiming to accomplish:

  • Map of the world continents (excluding Antarctica), shaded and filled
  • Highlight the timezone offset (not the timezone) on click
  • Display the name of the time zone that was clicked

Bonus items:

  • City lookup and reverse geo-coding (if you don’t mind a server component)
  • Size independent
  • As small (kb-wise) as possible
  • As few external dependencies as possible (preferably none)

Because this is quite the list of features, we’ll implement it in phases. For some of the trickier features, we’ll hopefully be able to get by without a server. We’ll have to wait and see. For phase 1, we’ll simply allow for the selection of specific time zones. In phase 2, we’ll extend it to city searching by reverse-geocoding.

Note: Our first instinct might be to check if someone else has done this for us already. Great! A quick good search reveals a few AngularJs pickers that already do much of what we want. Luckily for us, none are in Angular2 (yet!) Either way, that’s no fun! We need to do it ourselves. If we didn’t write it, it’s crap.

Our Custom Map

First things first, let’s get a map on which we can base our picker. Building a viable SVG map will be the primary focus of this post. To do so, we are going to use a nice set of Python scripts called Kartograph.

Kartograph actually has two parts, Kartograph.py and Kartograph.js. Kartograph.py is a Python library used to generate SVGs from GIS shape files. Kartograph.js is a javascript library for displaying and manipulating Kartograph.py maps in the browser. It does a great job, but in our case, we don’t need to incur the cost of the extra library, so we won’t use it. So we’ll just use Kartograph.py to generate ourselves a nice SVG.

A valid question at this point may be “Why incur the cost of an SVG when an image will do just fine?”. Great question. We’ll see some good reasons in the future as we implement more features, but let’s just say that we want the ultimate flexibilty. Our component should be able to allow the selection of time zones in any bounded geographic area, not just for a hard-coded map. Building our component around SVGs generated by Kartograph helps ensure users will be able to generate and use their own custom maps.

Other great reasons:

  • Different Projections
  • User styling
  • Attribution

Tangent: I had a roommate in college who studied GIS and I never even realized how cool it was. GIS deals with some interesting problems. Starting this component, I bet you didn’t realize you’d be learning GIS shape files, map projections, and reverse geo-coding.

The Map Shapes

If you haven’t installed Kartograph.py. Go ahead and do it now. You’ll need it for this part.

To kick things off, we’ll need some great shape (.shp) files to serve as the base for our map. Like usual, the open source community saves us here. For our picker, these are the shape files we will need:

For phase 1, we’ll only be using the first 2. But if you’re going to be around for the whole series, go ahead and download the last one as well.

If you want a free application that can view the .shp files before we turn them into SVGs, check out QGIS.

Generating Our Map

Alright, now for a crash course in using Kartograph.py. In it’s simplest form, Kartograph runs as a command line utility that accepts a configuration JSON file and generates an output SVG. The configuration files can include many different settings. Read the docs to see them all. We will use a subset while generating our map.

So let’s take a first stab at a configuration file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
  "layers" : {
      "land" : {
          "src" : "srcs/ne_110m_land.shp"
      },

      "timezones" : {
          "src" : "srcs/tz_world.shp"
      }
  },
  "proj": {
      "id": "lonlat"
  }
}

This configuration specifies that we want 2 layers. The “land” layer will come from the ne_110m_land.shp files, and the “timezones” layer will come from tz_world.shp. We also specify the map projection to be “lonlat”.

You can think of map projections as consistent ways of mapping the spherical latitude and longitude measurements to a 2d plane. If that peaks your interest at all, read through the wiki page. It’s another interesting area of GIS. The “lonlat” projection we are using is a linear interpolation algorithm that will make extracting lat/lng from our map much simpler when we implement reverse-geocoding.

So now that we have our configuration file setup, we run Kartograph, and get our output SVG. Let’s open it up and take a look. It looks great! We’ve got our continent outlines overlaid by our timezone outlines. Perfect.

So I guess we’re done! Ship it! Okay, not so fast. That sure took a while to load and render in Chrome when I opened it. What’s going on? Uh oh. Finder adds some insight…

Ouch! 46 megs?!? It’s large enough the OSX won’t even render a preview image. That might work for my good friends down in Provo that run Google Fiber, but I’m not so lucky. Let’s see what we can do to shrink it.

Taking a quick glance at the SVG code in an editor seems to indicate that the file size is due to the shear number of points necessary to represent each time zone boundary. In other words, our SVG is simply too complex. We have two options, eliminate shapes or simplify the geometry. We’ll try both.

Optimizing For Size – Eliminate Shapes

Scouring through the Kartograph docs, I noticed that there is a “filter” option that can be used in our configuration to conditionally include geometries. That seems to fit the ticket. But what do we filter on? Let’s fire up QGIS and take a look. Selecting a shape in tz_world shows us the attributes.

There isn’t much here, but we can make it work. It seems like a good compromise would be to filter out extremely small time zones, as the likelihood of the user being able to even click those zones on the map is minuscule. That may be upsetting for users in those specific time zones, but remember that this is phase 1. If we want support for all possible time zones, we can address it in phase 2. So lets change our configuration to filter out anything less than 3 sq km.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  "layers" : {
      "land" : {
          "src" : "srcs/ne_110m_land.shp"
      },

      "timezones" : {
          "src" : "srcs/tz_world.shp",
          "filter" : ["AREA", "greater", 3]
      }
  },
  "proj": {
      "id": "lonlat"
  }
}

The result? 29.5 megs. That’s a pretty good decrease, but obviously still not good enough. Let’s take a look at our second option, simplifying the geometry.

Optimizing For Size – Simplify Geometry

The tz_world shape file has a pretty high resolution for it’s boundary accuracy. We definitely don’t need that much accuracy on a map that is likely to be render at less than 1024px wide. If we were printing a classroom map, maybe, but not for our purposes here. We need a way to simplify the geometry without having to modify our shape files by hand. Luckily, Kartograph can do just that.

By the way, put Visvalingam‚Äôs algorithm (how the simplification works) on the list of “GIS things I didn’t know existed that were created by very smart people”.

Let’s add the simplify option to our shape file layers. The ne_110m_land.shp file already has 110m resolution, so we only need basic simplification there. The tz_world layer, however, can do for a bit more.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  "layers" : {
      "land" : {
          "src" : "srcs/ne_110m_land.shp",
          "simplify" : 1
      },

      "timezones" : {
          "src" : "srcs/tz_world.shp",
          "filter" : ["AREA", "greater", 3],
          "simplify" : 3
      }
  },
  "proj": {
      "id": "lonlat"
  }
}

Now we’re making real progress. 214 KB and the SVG still looks great. Looks like simplification was the ticket. However, we can still probably eek out a few more bytes.

Optimizing For Size – Rounding for a few more bytes

Examining the SVG code, you might notice that our paths are using very precise numbers for their points. If we rounded the precision to the hundreth, we could probably shave a few more bytes without really affecting the shape of the geometry. What’s that? Kartograph has the round option? Let’s plug it in.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
  "layers" : {
      "land" : {
          "src" : "srcs/ne_110m_land.shp",
          "simplify" : 1
      },

      "timezones" : {
          "src" : "srcs/tz_world.shp",
          "filter" : ["AREA", "greater", 3],
          "simplify" : 3
      }
  },
  "proj": {
      "id": "lonlat"
  },
  "export": {
     "round": 1
  }
}

Perfect! After all of our optimization, now we’re down to a respectable 118 KB. Much better. But what is that monstrosity on the bottom of our map? I’m going to go ahead and make the assumption that if OSX doesn’t include Antarctica on their map, we’re safe to exclude it as well. Now, we could just add another filter that explicitly excludes Antarctica from our SVG, but for learning sake, let’s use bounding to do it instead. Bounds allow us to specify the portion of the shape files that we actually want included in our SVG map.

Optimizing Bounds

In our case, we’ll just be trimming off some of the southern hemisphere. However, you could use this to isolate specific areas of the world for you own map. And best of all, Kartograph includes metadata in the SVG that can be used to determine the lat/lng boundaries of the current map! We’ll be able to use this in phase 2 for simple lat/lng lookup based on mouse click location.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
  "layers" : {
      "land" : {
          "src" : "srcs/ne_110m_land.shp",
          "simplify" : 1
      },

      "timezones" : {
          "src" : "srcs/tz_world.shp",
          "filter" : ["AREA", "greater", 3],
          "simplify" : 3
      }
  },
  "proj": {
      "id": "lonlat"
  },
  "export": {
     "round": 1
  },
  "bounds": {
      "mode": "bbox",
      "data": [-180, -60, 180, 90]
  }
}

Overall, we’re now down to 113 KB. Gzipped, we’re looking at 37 KB. We’re in JPG territory now, which is good enough for me. We might be able to eek out a few more bytes, but we’re probably at the point of diminishing returns. Plus, 37KB is already so much more manageable than 46 megs.

Our map looks much simpler now, and that is good. Once we style it, we’ll have a nice clean starting point for our component.

Adding Timezone Attribution

After all of our changes, however, we need to add one more configuration to our map before we finish. For easy lookup, we need to know the Olson code of the timezone that the user is clicking. Without doing a lat/lng lookup into a timezone database (we’ll get to that in later phases), the easiest way to allow this is to add the attribute to each time zone SVG path.

Luckily, as was observed previously, our tz_world shape file has the Olson code as an attribute on each time zone shape. So let’s tweak our configuration to have Kartograph maintain that attribute.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
  "layers" : {
      "land" : {
          "src" : "srcs/ne_110m_land.shp",
          "simplify" : 1
      },

      "timezones" : {
          "src" : "srcs/tz_world.shp",
          "filter" : ["AREA", "greater", 3],
          "simplify" : 3,
          "attributes": {
              "tz_id": "TZID"
          }
      }
  },
  "proj": {
      "id": "lonlat"
  },
  "export": {
     "round": 1
  },
  "bounds": {
      "mode": "bbox",
      "data": [-180, -60, 180, 90]
  }
}

Whew! We made it! We’ve got an SVG that can serve as the starting point for our phase 1 implementation. With the metadata we added, it increased the size just a bit, but it still clocks in at 39Kb gzipped. Not bad. In the next post, we’ll style the SVG, and get our Angular2 component up and running.

Spoiler Alert With reverse-geocoding, we won’t actually need the tz-world layer, which turns out to be the heaviest of the layers. We’ll be able to cut this SVG down in phase 2. So our SVG is likely to end up even smaller.

Conclusion

In the first part of our timezone picker series, we’ve started the process towards our amazing Angular2 timezone picker by using Kartograph to generate an SVG map. We optimized the size of the map by eliminating unnecessary geometry and simplifying preserved geometry. We also added attribution to time zone for ease of timezone lookup.

In the next entry, we’ll style our SVG make and write the Angular2 code to wire up the behaviors we want.

In future entries, we’ll expand our implementation to phase 2, reverse-geocoding and city lookup.

Thanks for reading! Follow on Twitter for subsequent posts.

—T

Comments