hxu/hk_census_explorer — pull request #21 by hxu (2014-02-02 09:52:42+00:00)
Title: Frontend map directive and some services

This branch implements a number of features. If anyone has time, please review and give comments. There are a few design decisions in here that I'm not so sure about (see bottom).

This fixes #5, #10, and #13.

#### `Mappings` Service

This services provides functions to go between Regions, Districts and Areas:

- `getDistrictsFromRegion` - Returns a promise that provides a list of districts when given a region code
- `getRegionFromDistrict` - Returns a promise that provides a region code when given a district
- `getAreasFromDistrict` - Returns a promise that provides a list of areas when given a district code
- `getDistrictFromArea` - Returns a promise that provides a district code when given an area code
- `getAllAreas` - Returns a promise that provides a list of all 412 districts

Note that all functions return promises. Not so sure about this design decision (see bottom for details). Also, going down the tree (Region -> district, district -> area) accepts a list of parents or string, and will returned the flattened set of children.

Internally, it stores a `_data` object, which has the list of all districts and regions initially, and is populated with the list of all areas.

#### `GeoFiles` Service

Handles the fetching and parsing of the district and area polygon files. I encoded the underlying json files as TopoJSON to save size (130kb vs. 4mb for the area polygons), so this requires an extra step with the topojson library, but this is handled by this service. Provides two functions:

- `getAreas` - Returns a promise that resolves with the area GeoJSON objects
- `getDistricts` - Returns a promise that resolves with the district GeoJSON objects

#### `AreaSelection` Service

This is a model that handles state when allowing users to add / remove areas from a selection. Basically only one function on this service:

- `getModel` - Gets a new instance of the model

Once you have an instance of the model (the AreaModel object), it has these methods on its prototype:

- `addArea` - Takes a single or list of region, district, and area codes, and adds them to the selection
- `removeArea` - Takes a single or list of region, district, and area codes, and removes them from the selection
- `clearSelected` - Clears all selected areas
- `selectedAreas` - Returns a sorted list of selected area codes
- `isSelected` - When given an area code, returns true if it is selected, false otherwise

Internally, it stores the selection as the `_selected` object. We use an object instead of a list to have set-like functionality. When you give it districts or regions, it has to get the mappings from `Mappings`, so the model may not update immediately, since it has to wait for the Mappings promises to resolve.

#### `hkMap` Directive

This puts it all together and provides instances of a a Leaflet map with our GeoJSON layers overlayed. Use like this:

```
# Optional, but recommended. Uses this object to keep selection state

```

Key features:
- Zooming out shows district polygons, zooming in shows areas when you get close enough
- Click to toggle selection of areas. Clicking on a district centers it and zooms in so you see areas

One current problem is that if you change the selection state from outside of the map, you have to emit a signal `redrawMap` in order for the map to update its layer styles.

See it in action at `/#/map`.

#### Easier conversion from HK1980 to WSG84

As it turns out, the `ogr2ogr` command line tool can easily convert from HK1980 to WSG84. See the `convert_shapefiles.sh` script for my notes on that. Basically if you know that HK1980 is commonly called `EPSG:2326`, then it's easy to convert.

I didn't go to the effort of making it an automated script, so you need to run these commands manually.

#### Miscellaneous

- Cleanup of unused/old JSON files
- Tests! `grunt test` runs single unit test, `grunt test:continuous` runs continuous testing. Didn't go for maximum coverage, mostly tested services and tricky parts of directives.

#### Remaining questions / points of discussion

- Should Mappings be using promises?

The full `geo-tree.js` is only 3kb uncompressed, so it actually wouldn't be so bad to just embed it in the code of the `Mappings` service. But I started out with as an asynchronous HTTP request, which means that you have to use lots of promises. Promises are OK, but it's a bit of potentially unnecessary complexity.

- Better way of keeping `hkMap` state synchronized

If you manipulate the selection state from outside of the `hkMap` directive (e.g. you add/remove selected areas not by clicking, but programatically), you have to remember to emit a signal to the `hkMap` directive.

I thought about making it a watch on the model object, but was afraid this would cause performance issues, because you have to loop through all the layers on the map to restyle them.

Anyone have any ideas?

- Switching between district and area map layers is maybe unnecessary?

Some of the larger districts, like Lantau and the Islands, don't always fit correctly when you zoom too far in, but you need to zoom in to choose individual areas. Maybe it's easier to just show the areas only?

- Can't currently select whole districts at once on the map

Need a way to select a whole district at once, but not sure how to do this.