Sunday, October 1, 2017

Angular Carto: Switching Basemaps

This post is part of a series covering how to create reusable Carto map components. Read the introduction to learn why one would even want to do such a thing. In this post, we will discuss how to give the user control of the basemap and how to do some customizations on the basemap selector.

Manipulating the basemap

Before we begin, we need to ask ourselves: what is a basemap, and what does it do? In the context of a Carto map, a basemap is a layer of map tiles that sits under the informational layers. In order to be able to switch between different basemaps, each basemap should know how to add itself to the Carto map and how to remove itself from the Carto map.

This functionality is implemented in the basemap prototype, cleverly named basemapProto. The prototype has two properties and two methods:

tileLayer
The Leaflet tile layer for the basemap.
map
The Carto/Leaflet map whose basemap is to be manipulated.
addToMap()
Add the basemap to the Carto/Leaflet map object.
removeFromMap()
Remove the basemap from the Carto/Leaflet map object.

Now that we’ve defined a prototype for the basemaps, we need a way of building new basemaps from the prototype. To do this, we can define a factory function, cleverly named basemap. This function takes three parameters:

map
The Carto/Leaflet map whose basemap is to be manipulated.
label
A descriptive label for the map to be used in the user interface.
tileLayer
The Leaflet tile layer for the basemap.

What are controllers?

We’ve written code to change the basemap, but we still need to build a user interface that calls this code. We will do this by writing a controller for basemap selection. Somewhat confusingly, the most common way of using Angular controllers is actually an example of the Model-View-View Model (MVVM for short) user interface architecture. Consequently, you will often see the phrases “controller” and “view model” used interchangeably. To avoid confusion, I will generally stick with “controller” when referring to an Angular component.

My favorite guide to the MVVM design pattern remains Martin Fowler’s, despite being over a decade old as of this writing.

The essence of a [view model] is of a fully self-contained class that represents all the data and behavior of the UI window, but without any of the controls used to render that UI on the screen. A view then simply projects the state of the [view model] onto the glass.

To do this the [view model] will have data fields for all the dynamic information of the view. This won’t just include the contents of controls, but also things like whether or not they are enabled. In general the [view model] does not need to hold all of this control state (which would be lot) but any state that may change during the interaction of the user. So if a field is always enabled, there won’t be extra data for its state in the [view model].

Since the [view model] contains data that the view needs to display the controls you need to synchronize the [view model] with the view.

Projecting the state onto the glass

Before we discuss the implementation of the controller, it will be useful to discuss how Angular “projects the state of the [controller] onto the glass.” The controller exposes two properties:

basemaps
The list of basemaps available for the user to select.
selectedBasemap
The currently selected basemap.
We can use Angular’s ng-options directive to bind the options in a drop-down box to the basemaps property. Let’s break down the meaning of the ng-options expression in the sample code; somewhat confusingly, the expression is read right-to-left:
  • basemap in ctrl.basemap means that the options in the drop-down box are bound to each of the items in the controller’s basemaps property. Within the expression each item in the list of basemaps is aliased as basemap.
  • basemap as basemap.label means that when projected on the glass, the label property of the basemap will be displayed to the user.

The Angular ng-model directive binds the currently selected item in the drop-down box to the selectedBasemap property of the controller.

To sum up, the ng-options and ng-model directives ensure that the available options in the drop-down box are bound to the controller’s basemaps property, and the selected option is bound to the selectedBasemap property.

The state that is to be projected

The function that creates basemap controller’s is cleverly named basemapController(). It takes one parameter: a list of basemaps for the basemaps property of the controller. Delegating the basemaps property to a parameter of the basemapController() function enhances code reusability. The logic for switching between basemaps is the same for every map we build, but the list of available basemaps is likely to vary from map to map.

The selectedBasemap property has a more complicated implementation. The property setter (line 68 below) first checks whether a basemap is currently selected; if so, it removes that basemap from the map by calling its removeFromMap() function and sets the basemap’s isSelected property to false. Next, it adds the newly selected basemap to the map by calling its addToMap() function and sets its isSelected property to true. Finally, it saves the newly selected basemap in the backing store of the property.

Last of all, if no basemap is selected the map will be unbelievably boring, so the basemapController() function selects the first basemap in the list before returning the controller (lines 85–87).

The list of basemaps

For this example, we will make three basemaps available for selection: Google imagery, OpenSteetMap, and the ArcGIS world topo map. To construct the tile layers, we use Leaflet’s L.tileLayer() function. The available list of basemaps is fully customizable by returning a different list from the getBasemaps() function.

Register the basemap components

To get the basemap selection feature up and running, we will have to register everything with Angular. We register the getBasemaps() function as a factory under the name “basemaps”. The basemapController() creation function is registered as a controller under the name “basemapController.” Thanks to Angular dependency injection, Angular will resolve the basemaps parameter of the basemapController function by looking up the “basemaps” name, which we registered previously.

Calling the basemap controller from the HTML

To use the basemap controller, we use the ng-controller directive to indicate that a particular controller should be used for an HTML element. The ng-controller expression BasemapController as ctrl indicates that Angular should use whatever controller is registered under the name “BasemapController”, and that the alias ctrl should be used within that HTML element to refer to the controller.

Customizing the appearance of the basemap selector

A dropdown list is not the only choice available for controlling basemap selection. Another option is to use the ng-repeat directive to make a stack of buttons instead. The astute reader will note that the basemap’s isSelected property is not used by the dropdown, but is used by the button repeater.

Bringing it all together

You can Fiddle around with a working Angular Carto basemap selector below.

Why would you do such a thing?

I don’t want to have to rewrite the logic for selecting basemaps from scratch every time, especially because the only thing that ever changes is the list of basemaps, not the selection logic. Do you want to?

Design pattern bingo

  • Angular controller
  • Angular dependency injection
  • Angular provider (factory)

Other posts in this series

No comments:

Post a Comment