[tilemill] 04/06: Imported Upstream version 0.10.1+ds1

Ross Gammon ross-guest at moszumanska.debian.org
Mon Mar 31 20:01:42 UTC 2014


This is an automated email from the git hooks/post-receive script.

ross-guest pushed a commit to branch master
in repository tilemill.

commit a96c142f2cf132f5d4c7daa3f5b222902a77f654
Author: Ross Gammon <rossgammon at mail.dk>
Date:   Mon Mar 31 21:50:26 2014 +0200

    Imported Upstream version 0.10.1+ds1
---
 .gitignore                                         |   4 +
 .gitmodules                                        |   6 -
 README.md                                          |  42 +-
 _config.yml                                        |  27 -
 assets/css/codemirror.css                          | 132 +++-
 assets/css/controls.css                            |  13 +
 assets/css/style.css                               |  63 +-
 assets/images/controls_2x.png                      | Bin 0 -> 740 bytes
 assets/images/favicon.ico                          | Bin 374 -> 22509 bytes
 assets/images/sprite.png                           | Bin 22963 -> 23456 bytes
 assets/images/sprite.svg                           |  46 +-
 assets/images/sprite_2x.png                        | Bin 0 -> 50837 bytes
 assets/images/tilemill_2x.png                      | Bin 0 -> 1327 bytes
 assets/js/codemirror.carto.complete.js             |   6 +-
 assets/js/codemirror.carto.js                      |   9 +-
 commands/core.bones                                | 114 ++--
 commands/export.bones                              | 473 +++++++++++----
 commands/global.bones                              |  16 +-
 commands/start.bones                               |  83 ++-
 commands/tile.bones                                |   2 +-
 controllers/Router.bones                           |  68 ++-
 examples/open-streets-dc/.thumb.png                | Bin 70237 -> 68203 bytes
 examples/open-streets-dc/highway.mss               | 662 ++++++++-------------
 examples/open-streets-dc/images/water.png          | Bin 0 -> 830 bytes
 examples/open-streets-dc/images/wood.png           | Bin 0 -> 402 bytes
 examples/open-streets-dc/labels.mss                |  72 +--
 examples/open-streets-dc/layers/dc-coastline.dbf   | Bin 0 -> 89 bytes
 examples/open-streets-dc/layers/dc-coastline.prj   |   1 +
 examples/open-streets-dc/layers/dc-coastline.shp   | Bin 0 -> 24756 bytes
 examples/open-streets-dc/layers/dc-coastline.shx   | Bin 0 -> 116 bytes
 .../layers/dc_osm_highway/dc_osm_highway.dbf       | Bin 642349 -> 0 bytes
 .../layers/dc_osm_highway/dc_osm_highway.index     | Bin 157164 -> 0 bytes
 .../layers/dc_osm_highway/dc_osm_highway.prj       |   1 -
 .../layers/dc_osm_highway/dc_osm_highway.shp       | Bin 1981852 -> 0 bytes
 .../layers/dc_osm_highway/dc_osm_highway.shx       | Bin 56556 -> 0 bytes
 .../layers/dc_osm_location/dc_osm_location.dbf     | Bin 1460078 -> 0 bytes
 .../layers/dc_osm_location/dc_osm_location.index   | Bin 134844 -> 0 bytes
 .../layers/dc_osm_location/dc_osm_location.prj     |   1 -
 .../layers/dc_osm_location/dc_osm_location.shp     | Bin 329744 -> 0 bytes
 .../layers/dc_osm_location/dc_osm_location.shx     | Bin 94284 -> 0 bytes
 .../layers/dc_osm_natural/dc_osm_natural.dbf       | Bin 67877 -> 0 bytes
 .../layers/dc_osm_natural/dc_osm_natural.index     | Bin 56856 -> 0 bytes
 .../layers/dc_osm_natural/dc_osm_natural.prj       |   1 -
 .../layers/dc_osm_natural/dc_osm_natural.shp       | Bin 1313036 -> 0 bytes
 .../layers/dc_osm_natural/dc_osm_natural.shx       | Bin 10732 -> 0 bytes
 examples/open-streets-dc/layers/osm-landusages.dbf | Bin 0 -> 7264078 bytes
 examples/open-streets-dc/layers/osm-landusages.prj |   1 +
 examples/open-streets-dc/layers/osm-landusages.shp | Bin 0 -> 5767120 bytes
 examples/open-streets-dc/layers/osm-landusages.shx | Bin 0 -> 100812 bytes
 examples/open-streets-dc/layers/osm-mainroads.dbf  | Bin 0 -> 1631053 bytes
 examples/open-streets-dc/layers/osm-mainroads.prj  |   1 +
 examples/open-streets-dc/layers/osm-mainroads.shp  | Bin 0 -> 462176 bytes
 examples/open-streets-dc/layers/osm-mainroads.shx  | Bin 0 -> 16068 bytes
 examples/open-streets-dc/layers/osm-motorways.dbf  | Bin 0 -> 720915 bytes
 examples/open-streets-dc/layers/osm-motorways.prj  |   1 +
 examples/open-streets-dc/layers/osm-motorways.shp  | Bin 0 -> 178872 bytes
 examples/open-streets-dc/layers/osm-motorways.shx  | Bin 0 -> 7156 bytes
 examples/open-streets-dc/layers/osm-places.dbf     | Bin 0 -> 82513 bytes
 examples/open-streets-dc/layers/osm-places.prj     |   1 +
 examples/open-streets-dc/layers/osm-places.shp     | Bin 0 -> 4244 bytes
 examples/open-streets-dc/layers/osm-places.shx     | Bin 0 -> 1284 bytes
 examples/open-streets-dc/layers/osm-roads.dbf      | Bin 0 -> 6466777 bytes
 examples/open-streets-dc/layers/osm-roads.prj      |   1 +
 examples/open-streets-dc/layers/osm-roads.shp      | Bin 0 -> 2375816 bytes
 examples/open-streets-dc/layers/osm-roads.shx      | Bin 0 -> 85748 bytes
 examples/open-streets-dc/layers/osm-waterareas.dbf | Bin 0 -> 62685 bytes
 examples/open-streets-dc/layers/osm-waterareas.prj |   1 +
 examples/open-streets-dc/layers/osm-waterareas.shp | Bin 0 -> 252880 bytes
 examples/open-streets-dc/layers/osm-waterareas.shx | Bin 0 -> 1028 bytes
 examples/open-streets-dc/project.mml               | 224 +++++--
 examples/open-streets-dc/style.mss                 |  66 +-
 examples/road-trip/.thumb.png                      | Bin 58987 -> 58222 bytes
 examples/road-trip/project.mml                     |   4 +-
 index.js                                           |  23 +-
 lib/config.defaults.json                           |   6 +-
 lib/crashutil.js                                   |  51 ++
 lib/fsutil.js                                      |  19 +-
 lib/gitutil.js                                     |  13 +
 lib/redirect.js                                    |  18 +
 lib/s3.js                                          |  28 +-
 lib/ubuntu_gui_workaround.js                       |  74 +++
 models/Config.bones                                |  10 +-
 models/Config.server.bones                         |  14 +-
 models/Datasource.server.bones                     |  92 ++-
 models/Export.bones                                |   4 +
 models/Exports.server.bones                        |  69 ++-
 models/Layer.bones                                 |   8 +
 models/Library.server.bones                        |  75 ++-
 models/Plugin.server.bones                         |   6 +-
 models/Plugins.server.bones                        |  17 +-
 models/Preview.server.bones                        |   1 +
 models/Project.bones                               |  38 +-
 models/Project.server.bones                        |  99 ++-
 package.json                                       |  57 +-
 plugins/carto/templates/Reference._                |  12 +-
 plugins/editor/views/Stylesheet.bones              |   6 +-
 plugins/editor/views/Stylesheets.bones             |  53 +-
 plugins/map/package.json                           |   6 -
 plugins/templates/templates/Templates._            |   2 +-
 plugins/templates/views/Templates.bones            |  18 +-
 servers/App.bones                                  |  54 +-
 servers/Core.bones                                 |   1 +
 servers/OAuth.bones                                |  78 +--
 servers/Tile.bones                                 |  13 +-
 templates/Autostyle._                              |   2 +-
 templates/Config._                                 |  32 +-
 templates/Layer._                                  |  33 +-
 {plugins/map/templates => templates}/Map._         |   0
 templates/Metadata._                               |  28 +-
 templates/MetadataSignup._                         |  18 +
 templates/Plugin._                                 |   6 +
 templates/Plugins._                                |   8 +-
 templates/Project._                                |   5 +-
 templates/ProjectLayer._                           |   4 +-
 test/abilities.test.js                             |  91 +--
 test/config.test.js                                | 198 +++---
 test/datasource.test.js                            | 154 +++--
 test/duplicate_module.test.js                      |  25 +
 test/export.test.js                                | 139 +++--
 test/fixtures/created-project.json                 |   2 +
 test/fixtures/datasource-postgis.json              |  10 +-
 test/fixtures/datasource-shp-features.json         |   2 +-
 test/fixtures/datasource-sqlite.json               |  10 +-
 test/project.test.js                               | 274 +++++----
 test/support/start.js                              | 191 ++++--
 test/tile.test.js                                  |  81 ++-
 tilemill.ico                                       | Bin 0 -> 108366 bytes
 views/App.bones                                    |   6 +-
 views/Config.bones                                 |  33 +-
 views/Layer.bones                                  |  96 ++-
 views/Layers.bones                                 |  67 ++-
 views/Library.bones                                |   1 +
 {plugins/map/views => views}/Map.bones             |  51 +-
 views/Metadata.bones                               |  54 +-
 views/Modal.bones                                  |  12 +-
 views/Plugins.bones                                |  23 +-
 views/Preview.bones                                |   9 +-
 views/Project.bones                                |  38 +-
 138 files changed, 3220 insertions(+), 1590 deletions(-)

diff --git a/.gitignore b/.gitignore
index aac0760..409a190 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,7 @@
 /test/fixtures/files
 npm-debug.log
 VERSION
+platforms/osx/Externals/Sparkle/source
+examples/geography-class/layers
+examples/road-trip/layers
+examples/control-room/layers
diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644
index 597102e..0000000
--- a/.gitmodules
+++ /dev/null
@@ -1,6 +0,0 @@
-[submodule "platforms/osx/Externals/FMDB"]
-	path = platforms/osx/Externals/FMDB
-	url = https://github.com/ccgus/fmdb.git
-[submodule "platforms/osx/Externals/JSONKit"]
-	path = platforms/osx/Externals/JSONKit
-	url = https://github.com/johnezang/JSONKit.git
diff --git a/README.md b/README.md
index 7e7800e..315007b 100644
--- a/README.md
+++ b/README.md
@@ -8,10 +8,10 @@ on the [TileMill website](http://mapbox.com/tilemill).
 
 # Running tests
 
-Install expresso and run the tests
+Install mocha and run the tests
 
-   npm install expresso
-   npm test
+    npm install mocha
+    npm test
 
 
 Note: the tests require a running postgres server and a postgis enabled
@@ -30,16 +30,42 @@ If you do not have a `template_postgis` create one like:
 For more info see: http://postgis.refractions.net/documentation/manual-1.5/ch02.html#id2619431
 
 
-# Viewing docs locally
+# Documentation
 
-## Install jekyll
+Tilemill documentation is kept in the gh-pages branch, which is independently managed and not merged with master.
+
+Tilemill's in-app reference available as the "Manual" is a very small subset of docs for offline usage and is manually
+sync'ed from the gh-pages branch.
+
+To view all the TileMill documentation locally, first checkout the gh-pages branch:
+
+    git checkout gh-pages
+
+Then install Jekyll:
 
     sudo gem install jekyll
 
-## Run jekyll
+And run Jekyll:
 
     jekyll
 
-## View the site at:
+Once Jekyll has started you should be able to view the docs in a browser at:
+
+    http://localhost:4000/tilemill/
+
+
+# Syncing manual
+
+To sync the manual with gh-pages updates do:
 
-    http://localhost:4000/tilemill/docs/
+    export TILEMILL_SOURCES=`pwd`
+    cd ../
+    git clone --depth=1 -b gh-pages https://github.com/mapbox/tilemill tilemill-gh-pages
+    cd ${TILEMILL_SOURCES}
+    export TILEMILL_GHPAGES=../tilemill-gh-pages
+    rm -rf ${TILEMILL_SOURCES}/assets/manual
+    mkdir -p ${TILEMILL_SOURCES}/assets/manual
+    cp -r ${TILEMILL_GHPAGES}/assets/manual/* ${TILEMILL_SOURCES}/assets/manual/
+    rm -rf ${TILEMILL_SOURCES}/_posts/docs/reference
+    mkdir -p ${TILEMILL_SOURCES}/_posts/docs/reference
+    cp -r ${TILEMILL_GHPAGES}/_posts/docs/reference/* ${TILEMILL_SOURCES}/_posts/docs/reference/
diff --git a/_config.yml b/_config.yml
deleted file mode 100644
index bfaa064..0000000
--- a/_config.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-auto: true
-server: true
-exclude:
-# Directories
-- commands
-- controllers
-- data
-- examples
-- lib
-- models
-- node_modules
-- platforms
-- servers
-- templates
-- test
-- views
-# Files
-- .gitignore
-- .gitmodules
-- index.js
-- package.json
-- LICENSE.md
-- README.md
-baseurl: /tilemill
-permalink: /:categories/:title
-title: TileMill
-url: http://mapbox.com
diff --git a/assets/css/codemirror.css b/assets/css/codemirror.css
index d93e72d..05ad0ed 100644
--- a/assets/css/codemirror.css
+++ b/assets/css/codemirror.css
@@ -1,6 +1,11 @@
 .CodeMirror {
   line-height: 1em;
   font-family: monospace;
+
+  /* Necessary so the scrollbar can be absolutely positioned within the wrapper on Lion. */
+  position: relative;
+  /* This prevents unwanted scrollbars from showing up on the body and wrapper in IE. */
+  overflow: hidden;
 }
 
 .CodeMirror-scroll {
@@ -9,10 +14,41 @@
   /* This is needed to prevent an IE[67] bug where the scrolled content
      is visible outside of the scrolling box. */
   position: relative;
+  outline: none;
+}
+
+/* Vertical scrollbar */
+.CodeMirror-scrollbar {
+  position: absolute;
+  right: 0; top: 0;
+  overflow-x: hidden;
+  overflow-y: scroll;
+  z-index: 5;
+}
+.CodeMirror-scrollbar-inner {
+  /* This needs to have a nonzero width in order for the scrollbar to appear
+     in Firefox and IE9. */
+  width: 1px;
+}
+.CodeMirror-scrollbar.cm-sb-overlap {
+  /* Ensure that the scrollbar appears in Lion, and that it overlaps the content
+     rather than sitting to the right of it. */
+  position: absolute;
+  z-index: 1;
+  float: none;
+  right: 0;
+  min-width: 12px;
+}
+.CodeMirror-scrollbar.cm-sb-nonoverlap {
+  min-width: 12px;
+}
+.CodeMirror-scrollbar.cm-sb-ie7 {
+  min-width: 18px;
 }
 
 .CodeMirror-gutter {
   position: absolute; left: 0; top: 0;
+  z-index: 10;
   background-color: #f7f7f7;
   border-right: 1px solid #eee;
   min-width: 2em;
@@ -22,9 +58,13 @@
   color: #aaa;
   text-align: right;
   padding: .4em .2em .4em .4em;
+  white-space: pre !important;
+  cursor: default;
 }
 .CodeMirror-lines {
   padding: .4em;
+  white-space: pre;
+  cursor: text;
 }
 
 .CodeMirror pre {
@@ -38,30 +78,96 @@
   padding: 0; margin: 0;
   white-space: pre;
   word-wrap: normal;
+  line-height: inherit;
+  color: inherit;
+}
+
+.CodeMirror-wrap pre {
+  word-wrap: break-word;
+  white-space: pre-wrap;
+  word-break: normal;
+}
+.CodeMirror-wrap .CodeMirror-scroll {
+  overflow-x: hidden;
 }
 
 .CodeMirror textarea {
-  font-family: inherit !important;
-  font-size: inherit !important;
+  outline: none !important;
 }
 
-.CodeMirror-cursor {
+.CodeMirror pre.CodeMirror-cursor {
   z-index: 10;
   position: absolute;
   visibility: hidden;
-  border-left: 1px solid black !important;
+  border-left: 1px solid black;
+  border-right: none;
+  width: 0;
+}
+.cm-keymap-fat-cursor pre.CodeMirror-cursor {
+  width: auto;
+  border: 0;
+  background: transparent;
+  background: rgba(0, 200, 0, .4);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#6600c800, endColorstr=#4c00c800);
 }
-.CodeMirror-focused .CodeMirror-cursor {
+/* Kludge to turn off filter in ie9+, which also accepts rgba */
+.cm-keymap-fat-cursor pre.CodeMirror-cursor:not(#nonsense_id) {
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
+}
+.CodeMirror pre.CodeMirror-cursor.CodeMirror-overwrite {}
+.CodeMirror-focused pre.CodeMirror-cursor {
   visibility: visible;
 }
 
-span.CodeMirror-selected {
-  background: #ccc !important;
-  color: HighlightText !important;
-}
-.CodeMirror-focused span.CodeMirror-selected {
-  background: Highlight !important;
+div.CodeMirror-selected { background: #d9d9d9; }
+.CodeMirror-focused div.CodeMirror-selected { background: #d7d4f0; }
+
+.CodeMirror-searching {
+  background: #ffa;
+  background: rgba(255, 255, 0, .4);
 }
 
-.CodeMirror-matchingbracket {color: #0f0 !important;}
-.CodeMirror-nonmatchingbracket {color: #f22 !important;}
+/* Default theme */
+
+.cm-s-default span.cm-keyword {color: #708;}
+.cm-s-default span.cm-atom {color: #219;}
+.cm-s-default span.cm-number {color: #164;}
+.cm-s-default span.cm-def {color: #00f;}
+.cm-s-default span.cm-variable {color: black;}
+.cm-s-default span.cm-variable-2 {color: #05a;}
+.cm-s-default span.cm-variable-3 {color: #085;}
+.cm-s-default span.cm-property {color: black;}
+.cm-s-default span.cm-operator {color: black;}
+.cm-s-default span.cm-comment {color: #a50;}
+.cm-s-default span.cm-string {color: #a11;}
+.cm-s-default span.cm-string-2 {color: #f50;}
+.cm-s-default span.cm-meta {color: #555;}
+.cm-s-default span.cm-error {color: #f00;}
+.cm-s-default span.cm-qualifier {color: #555;}
+.cm-s-default span.cm-builtin {color: #30a;}
+.cm-s-default span.cm-bracket {color: #997;}
+.cm-s-default span.cm-tag {color: #170;}
+.cm-s-default span.cm-attribute {color: #00c;}
+.cm-s-default span.cm-header {color: blue;}
+.cm-s-default span.cm-quote {color: #090;}
+.cm-s-default span.cm-hr {color: #999;}
+.cm-s-default span.cm-link {color: #00c;}
+
+span.cm-header, span.cm-strong {font-weight: bold;}
+span.cm-em {font-style: italic;}
+span.cm-emstrong {font-style: italic; font-weight: bold;}
+span.cm-link {text-decoration: underline;}
+
+span.cm-invalidchar {color: #f00;}
+
+div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
+div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
+
+ at media print {
+
+  /* Hide the cursor when printing */
+  .CodeMirror pre.CodeMirror-cursor {
+    visibility: hidden;
+  }
+
+}
diff --git a/assets/css/controls.css b/assets/css/controls.css
index a9bc9e3..32ee2d8 100644
--- a/assets/css/controls.css
+++ b/assets/css/controls.css
@@ -74,6 +74,19 @@
   -webkit-box-shadow:rgba(0,0,0,0.1) 0px 1px 3px;
   }
 
+
+ at media only screen and (-webkit-min-device-pixel-ratio: 1.5),
+  only screen and (-o-min-device-pixel-ratio: 3/2),
+  only screen and (min--moz-device-pixel-ratio: 1.5),
+  only screen and (min-device-pixel-ratio: 1.5) {
+    .wax-tooltip .close,
+    .wax-fullscreen,
+    .zoomer {
+      background-image:url(../images/controls_2x.png);
+      background-size: 120px 30px;
+    }
+}
+
 .zoomout {
   background-position:-61px -1px;
   border-radius:3px 0px 0px 3px;
diff --git a/assets/css/style.css b/assets/css/style.css
index 54b1e01..51c3822 100644
--- a/assets/css/style.css
+++ b/assets/css/style.css
@@ -69,6 +69,19 @@ body.loading > .mask { position:fixed; }
 
 .scrolling { overflow:auto; }
 
+.warning-red {color:red;}
+
+.warning {
+  padding:4px;
+  border:1px solid #DC3F19;
+  box-shadow:rgba(0,0,0,.1) 0px 1px 3px;
+  }
+
+.warning-text {
+  color:#DC3F19;
+  font-size:11px;
+  }
+
 a {
   color:#222;
   text-decoration:none;
@@ -200,6 +213,7 @@ small { font-size:11px; }
 
 #modal .buttons input {
   background:#222;
+  color:#fff;
   border-color:#666;
   line-height:30px;
   height:30px;
@@ -375,6 +389,10 @@ small { font-size:11px; }
 
 .header h1 { font-size:16px; }
 
+.header .name { font-size:16px; }
+
+.header .project-status { color: #62A0AF; padding: 10px; font-size:12px; }
+
 .header .spinner {
   position:fixed;
   right:215px;
@@ -627,13 +645,17 @@ ul.layers {
 .layers li {
   position:relative;
   border-bottom:1px solid rgba(0,0,0,0.1);
-  padding:5px 70px 4px 30px;
+  padding:5px 130px 4px 30px;
   overflow:hidden;
   white-space:nowrap;
   min-width:60px;
   max-width:240px;
   }
 
+.layers li.status-off { color:#999; }
+.layers li.status-off .icon.geometry { opacity:0.5; }
+.layers li.status-off .icon.visibility { background-position:-700px -180px; }
+
 .layers .ui-sortable-helper {
   background:#fe8;
   padding-top:4px;
@@ -934,8 +956,9 @@ li:hover .icon { display:block; }
   }
 
 .icon.edit           { background-position:0px -180px; }
-.icon.inspect        { background-position:-20px -180px; }
+.icon.extent         { background-position:-20px -180px; }
 .icon.delete         { background-position:-60px -180px; }
+.icon.visibility     { background-position:-640px -180px; }
 .icon.color-picker   { background-position:-40px -180px; }
 .icon.close          { background-position:-80px -180px; }
 .icon.fonts          { background-position:-100px -180px; }
@@ -967,6 +990,7 @@ li:hover .icon { display:block; }
 .icon.cloud             { background-position:-620px -180px; }
 .icon.eye               { background-position:-640px -180px; }
 .icon.upload            { background-position:-680px -180px; }
+.icon.inspect           { background-position:-720px -180px; }
 
 .reverse.edit           { background-position:0px -200px; }
 .reverse.inspect        { background-position:-20px -200px; }
@@ -1218,11 +1242,13 @@ ul.form li label {
   width:100px;
   margin-left:-110px;
   text-align:right;
+  white-space:nowrap;
   }
 
 ul.form .buttons { text-align:center; }
 
 .button.large,
+ul.form .buttons .button,
 ul.form .buttons input {
   display:inline-block;
   padding-top:4px;
@@ -1553,6 +1579,7 @@ div.fonts {
 .exports li.error {
   color:#999;
   background:#f8f8f8;
+  height: 40px;
   }
 
 .exports .i40 {
@@ -1810,3 +1837,35 @@ ul.grid li {
 .exports .status a,
 .features .buttons a,
 .pane .pane-actions a { position:relative; }
+
+/* Swap retina graphics */
+ at media only screen and (-webkit-min-device-pixel-ratio: 1.5),
+  only screen and (-o-min-device-pixel-ratio: 3/2),
+  only screen and (min--moz-device-pixel-ratio: 1.5),
+  only screen and (min-device-pixel-ratio: 1.5) {
+
+  .icon,
+  .wax-point-div,
+  .swatch,
+  .swatch .color,
+  #colorpicker .caret,
+  .palette a.active:after {
+    background-image:url(../images/sprite_2x.png);
+    background-size: 800px 480px;
+  }
+
+  .about {
+    background-image:url(/assets/tilemill/images/tilemill_2x.png);
+    background-size: 40px 45px;
+  }
+
+  .loading:after {
+    background-image:url(../images/spinner-reverse_2x.gif);
+    background-size: 20px 20px;
+  }
+
+  .header .spinner {
+    background-image:url(../images/spinner_2x.gif);
+    background-size: 20px 20px;
+  }
+}
diff --git a/assets/images/controls_2x.png b/assets/images/controls_2x.png
new file mode 100644
index 0000000..74e1b70
Binary files /dev/null and b/assets/images/controls_2x.png differ
diff --git a/assets/images/favicon.ico b/assets/images/favicon.ico
index 4a4d650..4e1e385 100644
Binary files a/assets/images/favicon.ico and b/assets/images/favicon.ico differ
diff --git a/assets/images/sprite.png b/assets/images/sprite.png
index e6ac863..290c4ab 100644
Binary files a/assets/images/sprite.png and b/assets/images/sprite.png differ
diff --git a/assets/images/sprite.svg b/assets/images/sprite.svg
index f176d65..41c6e9a 100644
--- a/assets/images/sprite.svg
+++ b/assets/images/sprite.svg
@@ -14,7 +14,7 @@
    height="480"
    id="svg2"
    sodipodi:version="0.32"
-   inkscape:version="0.48.2 r9819"
+   inkscape:version="0.48.3.1 r9886"
    version="1.0"
    sodipodi:docname="sprite.svg"
    inkscape:output_extension="org.inkscape.output.svg.inkscape"
@@ -6046,19 +6046,19 @@
      objecttolerance="10"
      inkscape:pageopacity="0.0"
      inkscape:pageshadow="2"
-     inkscape:zoom="1"
-     inkscape:cx="313.15191"
-     inkscape:cy="147.17623"
+     inkscape:zoom="1.4142136"
+     inkscape:cx="442.29962"
+     inkscape:cy="298.48619"
      inkscape:document-units="px"
      inkscape:current-layer="layer1"
-     showgrid="false"
+     showgrid="true"
      inkscape:snap-bbox="true"
-     showguides="false"
+     showguides="true"
      inkscape:guide-bbox="true"
      inkscape:window-width="1343"
-     inkscape:window-height="817"
+     inkscape:window-height="715"
      inkscape:window-x="10"
-     inkscape:window-y="5"
+     inkscape:window-y="24"
      inkscape:window-maximized="0"
      inkscape:snap-nodes="true"
      inkscape:bbox-nodes="false"
@@ -7185,5 +7185,35 @@
        d="m 309,263.5 0,10 2.625,-2.1875 L 314,276.5 315,276 312.65625,270.875 315.75,270.25 309,263.5 z"
        id="path6580"
        inkscape:connector-curvature="0" />
+    <rect
+       style="color:#000000;fill:#202020;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+       id="rect4871"
+       width="2"
+       height="2"
+       x="649"
+       y="189"
+       transform="translate(0,-80)" />
+    <path
+       style="color:#000000;fill:#202020;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.05500000000000000;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;opacity:0.6"
+       d="M 710 185 C 706 185 705 188 702 190 C 703.16821 190.7788 704.0181 191.71661 704.875 192.5625 L 707.0625 190.5 C 707.03382 190.33342 707 190.17477 707 190 C 707 188.34313 708.34313 187 710 187 C 710.21746 187 710.42067 187.01855 710.625 187.0625 L 712.34375 185.4375 C 711.67688 185.16516 710.92678 185 710 185 z M 714.84375 187.1875 L 712.84375 189.125 C 712.9314 189.40682 713 189.68934 713 190 C 713 191.65687 711.65687 193 710 193 C 709.64446 193 709.31647 192.92602 709 192.8125 [...]
+       id="path4887"
+       transform="translate(0,-80)" />
+    <path
+       style="fill:none;stroke:#202020;stroke-width:1.25;stroke-linecap:round;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+       d="m 715.5,184.5 -11,10.5"
+       id="path4891"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cc"
+       transform="translate(0,-80)" />
+    <path
+       inkscape:connector-curvature="0"
+       style="fill:#202020;fill-opacity:1"
+       d="m 725,104 c -0.554,0 -1,0.446 -1,1 l 0,2 2.5,0 5,0 4.5,0 0,-2 c 0,-0.554 -0.446,-1 -1,-1 l -10,0 z m -1,4 0,2 2,0 0,-2 -2,0 z m 3,0 0,2 4,0 0,-2 -4,0 z m 5,0 0,2 4,0 0,-2 -4,0 z m -8,3 0,2 2,0 0,-2 -2,0 z m 3,0 0,2 4,0 0,-2 -4,0 z m 5,0 0,2 4,0 0,-2 -4,0 z m -8,3 0,1 c 0,0.554 0.446,1 1,1 l 1,0 0,-2 -2,0 z m 3,0 0,2 4,0 0,-2 -4,0 z m 5,0 0,2 3,0 c 0.554,0 1,-0.446 1,-1 l 0,-1 -4,0 z"
+       id="rect4871-7" />
+    <path
+       inkscape:connector-curvature="0"
+       style="fill:#ffffff;fill-opacity:1"
+       d="m 725,124 c -0.554,0 -1,0.446 -1,1 l 0,2 2.5,0 5,0 4.5,0 0,-2 c 0,-0.554 -0.446,-1 -1,-1 l -10,0 z m -1,4 0,2 2,0 0,-2 -2,0 z m 3,0 0,2 4,0 0,-2 -4,0 z m 5,0 0,2 4,0 0,-2 -4,0 z m -8,3 0,2 2,0 0,-2 -2,0 z m 3,0 0,2 4,0 0,-2 -4,0 z m 5,0 0,2 4,0 0,-2 -4,0 z m -8,3 0,1 c 0,0.554 0.446,1 1,1 l 1,0 0,-2 -2,0 z m 3,0 0,2 4,0 0,-2 -4,0 z m 5,0 0,2 3,0 c 0.554,0 1,-0.446 1,-1 l 0,-1 -4,0 z"
+       id="rect4871-2" />
   </g>
 </svg>
diff --git a/assets/images/sprite_2x.png b/assets/images/sprite_2x.png
new file mode 100644
index 0000000..da08ea4
Binary files /dev/null and b/assets/images/sprite_2x.png differ
diff --git a/assets/images/tilemill_2x.png b/assets/images/tilemill_2x.png
new file mode 100644
index 0000000..2ff5b3c
Binary files /dev/null and b/assets/images/tilemill_2x.png differ
diff --git a/assets/js/codemirror.carto.complete.js b/assets/js/codemirror.carto.complete.js
index 7c2449a..a789f55 100644
--- a/assets/js/codemirror.carto.complete.js
+++ b/assets/js/codemirror.carto.complete.js
@@ -153,18 +153,20 @@
                 insert(completions[0]); return true;
             }
 
+            completions.sort();
+
             // Build the select widget
             var pos = editor.cursorCoords();
 
             sel.innerHTML = '';
             sel.multiple = true;
-            for (var i = 0; i < Math.min(completions.length, 10); ++i) {
+            for (var i = 0; i < completions.length; ++i) {
                 var opt = sel.appendChild(document.createElement('option'));
                 opt.appendChild(document.createTextNode(completions[i]));
             }
             sel.firstChild.selected = true;
             sel.selectedIndex = 0;
-            sel.size = Math.min(10, completions.length);
+            sel.size = completions.length;
             sel.style.height = '100px';
 
             widget.className = 'completions';
diff --git a/assets/js/codemirror.carto.js b/assets/js/codemirror.carto.js
index 31fbdda..2115032 100644
--- a/assets/js/codemirror.carto.js
+++ b/assets/js/codemirror.carto.js
@@ -47,9 +47,9 @@ CodeMirror.defineMode('carto', function(config, parserConfig) {
     } else if (ch == '/' && stream.eat('/')) {
       stream.skipToEnd();
       return ret("carto-comment", "comment");
+    } else if (ch == '=' && stream.eat('~')) {
+      return ret(null, 'compare');
     } else if (ch == '=') {
-      ret(null, 'compare');
-    } else if ((ch == '~' || ch == '|') && stream.eat('=')) {
       return ret(null, 'compare');
     } else if (ch == '\"' || ch == "'") {
       state.tokenize = tokenString(ch);
@@ -94,11 +94,12 @@ CodeMirror.defineMode('carto', function(config, parserConfig) {
     return function(stream, state) {
       var escaped = false, ch;
       while ((ch = stream.next()) !== undefined) {
-        if (ch == quote && !escaped)
+        if (ch == quote && !escaped) {
+          if (!escaped) state.tokenize = tokenBase;
           break;
+        }
         escaped = !escaped && ch == '\\';
       }
-      if (!escaped) state.tokenize = tokenBase;
       return ret('carto-string', 'string');
     };
   }
diff --git a/commands/core.bones b/commands/core.bones
index 1e4126f..06e96b3 100644
--- a/commands/core.bones
+++ b/commands/core.bones
@@ -3,11 +3,12 @@ var fsutil = require('../lib/fsutil');
 var path = require('path');
 var Step = require('step');
 var defaults = models.Config.defaults;
-var spawn = require('child_process').spawn;
 var mapnik = require('mapnik');
 var semver = require('semver');
 var os = require('os');
 var crypto = require('crypto');
+// node v6 -> v8 compatibility
+var existsSync = require('fs').existsSync || require('path').existsSync;
 
 command = Bones.Command.extend();
 
@@ -87,7 +88,7 @@ command.prototype.bootstrap = function(plugin, callback) {
 
     var settings = Bones.plugin.config;
     settings.host = false;
-    settings.files = path.resolve(settings.files);
+    settings.files = path.resolve(settings.files.replace(/^~/, process.env.HOME));
     settings.coreUrl = settings.coreUrl || 'localhost:' + settings.port;
     settings.tileUrl = settings.tileUrl || 'localhost:' + settings.tilePort;
 
@@ -118,18 +119,18 @@ command.prototype.bootstrap = function(plugin, callback) {
     };
 
     var configDir = path.join(process.env.HOME, '.tilemill');
-    if (!path.existsSync(configDir)) {
+    if (!existsSync(configDir)) {
         console.warn('Creating configuration dir %s', configDir);
         fsutil.mkdirpSync(configDir, 0755);
     }
 
-    if (!path.existsSync(settings.files)) {
+    if (!existsSync(settings.files)) {
         console.warn('Creating files dir %s', settings.files);
         fsutil.mkdirpSync(settings.files, 0755);
     }
     ['export', 'project', 'cache'].forEach(function(key) {
         var dir = path.join(settings.files, key);
-        if (!path.existsSync(dir)) {
+        if (!existsSync(dir)) {
             console.warn('Creating %s dir %s', key, dir);
             fsutil.mkdirpSync(dir, 0755);
             if (key === 'project' && settings.examples) {
@@ -160,35 +161,52 @@ command.prototype.bootstrap = function(plugin, callback) {
     ]).chain()
         .map(function(p, index) {
             try {
-            return fs.readdirSync(p).map(function(dir) {
+            return fs.readdirSync(p).filter(function(d) {
+                return d[0] !== '.';
+            }).map(function(dir) {
+                var data;
                 try {
-                var pkg = path.join(p, dir, 'package.json');
-                var data = JSON.parse(fs.readFileSync(pkg, 'utf8'));
-                data.core = index === 0;
-                data.id = data.name;
-
-                // Engines key missing.
-                if (!data.engines || !data.engines.tilemill) {
-                    console.warn('Plugin [%s] "engines" missing.',
-                        Bones.utils.colorize(data.name, 'red'));
-                    return false;
-                }
-                // Check that TileMill version satisfies plugin requirements.
-                // Pass data through such that the plugin can be shown in the
-                // UI as failing to satisfy requirements.
-                if (!semver.satisfies(Bones.plugin.abilities.tilemill.version, data.engines.tilemill)) {
-                    console.warn('Plugin [%s] requires TileMill %s.',
-                        Bones.utils.colorize(data.name, 'red'),
-                        data.engines.tilemill);
+                    var pkg = path.join(p, dir, 'package.json');
+                    data = JSON.parse(fs.readFileSync(pkg, 'utf8'));
+                    data.core = index === 0;
+                    data.id = data.name;
+
+                    // Engines key missing.
+                    if (!data.engines || !data.engines.tilemill) {
+                        console.warn('Plugin [%s] "engines" missing.',
+                            Bones.utils.colorize(data.name, 'red'));
+                        return false;
+                    }
+                    // Check that TileMill version satisfies plugin requirements.
+                    // Pass data through such that the plugin can be shown in the
+                    // UI as failing to satisfy requirements.
+                    if (!semver.satisfies(Bones.plugin.abilities.tilemill.version, data.engines.tilemill)) {
+                        console.warn('Plugin [%s] requires TileMill %s.',
+                            Bones.utils.colorize(data.name, 'red'),
+                            data.engines.tilemill);
+                        return data;
+                    }
+                    // Load plugin
+                    // NOTE: even broken plugins (ones that throw upon require) will likely get partially loaded here
+                    require('bones').load(path.join(p, dir));
+                    console.warn('Plugin [%s] loaded.', Bones.utils.colorize(data.name, 'green'));
                     return data;
+                } catch (err) {
+                    if (data && data.name) {
+                        // consider, as broken, plugins which partially loaded but threw so that
+                        // the user can know to uninstall them, because unloading is not possible
+                        data.broken = true;
+                        console.error('Plugin [' + data.name + '] unable to be loaded: ' + err.stack || err.toString());
+                        return data;
+                    } else {
+                        console.error('Plugin loading error: ' + err.stack || err.message);
+                        return false;
+                    }
                 }
-                // Load plugin.
-                require('bones').load(path.join(p, dir));
-                console.warn('Plugin [%s] loaded.', Bones.utils.colorize(data.name, 'green'));
-                return data;
-                } catch (e) { console.error(e); return false; }
             });
-            } catch(e) { return []; }
+            } catch(err) {
+                return [];
+            }
         })
         .flatten()
         .compact()
@@ -198,41 +216,7 @@ command.prototype.bootstrap = function(plugin, callback) {
         }, {})
         .value();
 
-    // Config values to save.
-    var attr = {};
-
-    // Skip latest TileMill version check if disabled or
-    // we've checked the npm repo in the past 24 hours.
-    var skip = !settings.updates || (settings.updatesTime > Date.now() - 864e5);
-    var npm = require('npm');
-    Step(function() {
-        if (skip) return this();
-
-        console.warn('Checking for new version of TileMill...');
-        npm.load({}, this);
-    }, function(err) {
-        if (skip || err) return this(err);
-
-        npm.localPrefix = path.join(process.env.HOME, '.tilemill');
-        npm.commands.view(['tilemill'], true, this);
-    }, function(err, resp) {
-        if (skip || err) return this(err);
-
-        if (!_(resp).size()) throw new Error('Latest TileMill package not found.');
-        if (!_(resp).toArray()[0].version) throw new Error('No version for TileMill package.');
-        console.warn('Latest version of TileMill is %s.', _(resp).toArray()[0].version);
-        attr.updatesVersion = _(resp).toArray()[0].version;
-        attr.updatesTime = Date.now();
-        this();
-    }, function(err) {
-        // Continue despite errors but log them to the console.
-        if (err) console.error(err);
-        // Save any config attributes.
-        if (_(attr).keys().length) (new models.Config).save(attr, {
-            error: function(m, err) { console.error(err); }
-        });
-        callback();
-    });
+    callback();
 };
 
 command.prototype.initialize = function(plugin, callback) {
diff --git a/commands/export.bones b/commands/export.bones
index cc1b509..117645c 100644
--- a/commands/export.bones
+++ b/commands/export.bones
@@ -3,8 +3,13 @@ var url = require('url');
 var path = require('path');
 var request = require('request');
 var crypto = require('crypto');
+var util = require('util');
 var Step = require('step');
 var http = require('http');
+var chrono = require('chrono');
+var crashutil = require('../lib/crashutil');
+// node v6 -> v8 compatibility
+var existsSync = require('fs').existsSync || require('path').existsSync;
 
 command = Bones.Command.extend();
 
@@ -17,7 +22,7 @@ command.options['format'] = {
 };
 
 command.options['bbox'] = {
-    'title': 'bbox=[bbox]',
+    'title': 'bbox=[xmin,ymin,xmax,ymax]',
     'description': 'Comma separated coordinates of bounding box to export.'
 };
 
@@ -53,6 +58,45 @@ command.options['log'] = {
     'description': 'Write crash logs to destination directory.'
 };
 
+command.options['quiet'] = {
+    'title': 'quiet',
+    'description': 'Suppresses progress output.'
+};
+
+command.options['scheme'] = {
+    'title': 'scheme=[scanline|pyramid|file]',
+    'description': 'Enumeration scheme that defines the order in which tiles will be rendered.',
+    'default': 'scanline'
+};
+
+command.options['job'] = {
+    'title': 'job=[file]',
+    'description': 'Store state in this file. If it exists, that job will be resumed.',
+    'default': false
+};
+
+command.options['list'] = {
+    'title': 'list=[file]',
+    'description': 'Provide a list file for filescheme render.',
+    'default': false
+};
+
+command.options['metatile'] = {
+    'title': 'metatile=[num]',
+    'description': 'Metatile size.'
+};
+
+command.options['scale'] = {
+    'title': 'scale=[num]',
+    'description': 'Scale factor'
+};
+
+command.options['concurrency'] = {
+    'title': 'concurrency=[num]',
+    'description': 'Number of exports that can be run concurrently.',
+    'default': 4
+};
+
 command.prototype.initialize = function(plugin, callback) {
     _(this).bindAll('error', 'put', 'complete');
 
@@ -61,27 +105,55 @@ command.prototype.initialize = function(plugin, callback) {
         _(opts).extend(JSON.parse(process.env.tilemillConfig));
     opts.files = path.resolve(opts.files);
     opts.project = plugin.argv._[1];
-    opts.filepath = path.resolve(plugin.argv._[2]);
+    var export_filename = plugin.argv._[2];
+    if (!export_filename) return plugin.help();
+    opts.filepath = path.resolve(export_filename.replace(/^~/,process.env.HOME));
     callback = callback || function() {};
     this.opts = opts;
+    var cmd = this;
 
-    // Write crash log
-    if (opts.log) {
-        process.on('uncaughtException', function(err) {
-            fs.writeFileSync(opts.filepath + '.crashlog', err.stack || err.toString());
-            process.exit(1);
+    // Note: this is reset again below, to reflect any changes in the output name
+    process.title = 'tm-' + path.basename(opts.filepath);
+
+    // Write export-specific crash log
+    process.on('uncaughtException', function(err) {
+        cmd.error(err, function() {
+            var crash_log = opts.filepath + '.crashlog';
+            if (opts.log) {
+                console.warn('Export process died, log written to: ' + crash_log);
+                fs.writeFileSync(crash_log, err.stack || err.toString());
+            } else {
+                console.warn('Export process died: ' + err.stack || err.toString());
+            }
+            // force exit here because cleanup in tilelive is not working leading to:
+            // Error: SQLITE_IOERR: disk I/O error
+            // https://github.com/mapbox/tilemill/issues/1360
+            process.exit(0);
         });
-    }
+    });
+
+    process.on('exit', function(code, signal) {
+        console.warn('Exiting process [' + process.title + ']');
+        if (code !== 0)
+        {
+            crashutil.display_crash_log(function(err,logname) {
+                if (err) {
+                    console.warn(err.stack || err.toString());
+                }
+                if (logname) {
+                    console.warn("[tilemill] Please post this crash log: '" + logname + "' to https://github.com/mapbox/tilemill/issues");
+                }
+            });
+        }
+    });
 
     // Validation.
     if (!opts.project || !opts.filepath) return plugin.help();
-    if (!path.existsSync(path.dirname(opts.filepath)))
+    if (!existsSync(path.dirname(opts.filepath)))
         return this.error(new Error('Export path does not exist: ' + path.dirname(opts.filepath)));
 
     // Format.
     if (!opts.format) opts.format = path.extname(opts.filepath).split('.').pop();
-    if (!_(['pdf', 'svg', 'png', 'mbtiles', 'upload', 'sync']).include(opts.format))
-        return this.error(new Error('Invalid format: ' + opts.format));
 
     // Convert string params into numbers.
     if (!_(opts.bbox).isUndefined())
@@ -94,9 +166,14 @@ command.prototype.initialize = function(plugin, callback) {
         opts.width = parseInt(opts.width, 10);
     if (!_(opts.height).isUndefined())
         opts.height = parseInt(opts.height, 10);
+    if (!_(opts.metatile).isUndefined())
+        opts.metatile = parseInt(opts.metatile, 10);
+    if (!_(opts.scale).isUndefined())
+        opts.scale = parseInt(opts.scale, 10);
 
     // Rename the output filepath using a random hash if file already exists.
-    if (path.existsSync(opts.filepath) && !_(['upload','sync']).include(opts.format)) {
+    if (existsSync(opts.filepath) &&
+        _(['png','pdf','svg','mbtiles']).include(opts.format)) {
         var hash = crypto.createHash('md5')
             .update(+new Date + '')
             .digest('hex')
@@ -115,24 +192,39 @@ command.prototype.initialize = function(plugin, callback) {
     if (opts.format === 'upload') return this[opts.format](this.complete);
 
     // Load project, localize and call export function.
-    var cmd = this;
     var model = new models.Project({id:opts.project});
     Step(function() {
+        if (!cmd.opts.quiet) process.stderr.write('Loading project...');
         Bones.utils.fetch({model:model}, this);
     }, function(err) {
-        if (err) throw err;
+        if (err) return cmd.error(err, function() {
+            process.stderr.write(err.stack || err.toString() + '\n');
+            process.exit(1);
+        });
+        if (!cmd.opts.quiet) process.stderr.write(' done.\n');
+        // Set the postgres connection pool size to # of cpus based on
+        // assumption of pool size in tilelive-mapnik.
+        model.get('Layer').each(function(l) {
+            if (l.attributes.Datasource && l.attributes.Datasource.dbname)
+                l.attributes.Datasource.max_size = require('os').cpus().length;
+        });
+        if (!cmd.opts.quiet) process.stderr.write('Localizing project...');
         model.localize(model.toJSON(), this);
     }, function(err) {
         if (err) return cmd.error(err, function() {
+            process.stderr.write(err.stack || err.toString() + '\n');
             process.exit(1);
         });
 
+        if (!cmd.opts.quiet) process.stderr.write(' done.\n');
         model.mml = _(model.mml).extend({
             name: model.mml.name || model.id,
             version: model.mml.version || '1.0.0',
             minzoom: !_(opts.minzoom).isUndefined() ? opts.minzoom : model.get('minzoom'),
             maxzoom: !_(opts.maxzoom).isUndefined() ? opts.maxzoom : model.get('maxzoom'),
-            bounds: !_(opts.bbox).isUndefined() ? opts.bbox : model.get('bounds')
+            bounds: !_(opts.bbox).isUndefined() ? opts.bbox : model.get('bounds'),
+            scale: !_(opts.scale).isUndefined() ? opts.scale : model.get('scale'),
+            metatile: !_(opts.metatile).isUndefined() ? opts.metatile : model.get('metatile')
         });
 
         // Unset map center if outside bounds.
@@ -147,19 +239,41 @@ command.prototype.initialize = function(plugin, callback) {
         })(model.mml.center, model.mml.bounds, model.mml.minzoom, model.mml.maxzoom);
         if (!validCenter) delete model.mml.center;
 
-        cmd[opts.format](model, cmd.complete);
+        switch (opts.format) {
+        case 'png':
+        case 'svg':
+        case 'pdf':
+            console.log('Rendering file');
+            cmd.image(model, cmd.complete);
+            break;
+        case 'upload':
+            console.log('Uploading new export');
+            cmd.upload(model, cmd.complete);
+            break;
+        case 'sync':
+            console.log('Syncing export with existing upload');
+            cmd.sync(model, cmd.complete);
+            break;
+        default:
+            console.log('Rendering export');
+            cmd.tilelive(model, cmd.complete);
+            break;
+        }
     });
 };
 
 command.prototype.complete = function(err, data) {
+    console.log('Completing export process');
     if (err) {
+        console.warn(err.stack || err.toString() + '\n');
         this.error(err, function() {
-            process.exit(1);
+            process.exit(0);
         });
     } else {
         data = _(data||{}).defaults({
             status: 'complete',
             progress: 1,
+            remaining: 0,
             updated: +new Date()
         });
         this.put(data, process.exit);
@@ -183,14 +297,13 @@ command.prototype.remaining = function(progress, started) {
 
 command.prototype.put = function(data, callback) {
     callback = callback || function() {};
-    data.status == 'error' ?
-        console.error(JSON.stringify(data)) :
-        console.log(JSON.stringify(data));
 
     // Allow commands to filter.
     if (this.putFilter) data = this.putFilter(data);
 
-    if (!this.opts.url) return callback();
+    if (!this.opts.url) {
+        return callback();
+    }
     request.put({
         uri: this.opts.url,
         headers: {
@@ -201,9 +314,46 @@ command.prototype.put = function(data, callback) {
     }, callback);
 };
 
-command.prototype.png =
-command.prototype.svg =
-command.prototype.pdf = function(project, callback) {
+
+function formatDuration(duration) {
+    duration = duration / 1000 | 0;
+    var seconds = duration % 60;
+    duration -= seconds;
+    var minutes = (duration % 3600) / 60;
+    duration -= minutes * 60;
+    var hours = (duration % 86400) / 3600;
+    duration -= hours * 3600;
+    var days = duration / 86400;
+
+    return (days > 0 ? days + 'd ' : '') +
+           (hours > 0 || days > 0 ? hours + 'h ' : '') +
+           (minutes > 0 || hours > 0 || days > 0 ? minutes + 'm ' : '') +
+           seconds + 's';
+}
+
+function formatNumber(num) {
+    num = num || 0;
+    if (num >= 1e6) {
+        return (num / 1e6).toFixed(2) + 'm';
+    } else if (num >= 1e3) {
+        return (num / 1e3).toFixed(1) + 'k';
+    } else {
+        return num.toFixed(0);
+    }
+    return num.join('.');
+}
+
+function formatString(string) {
+    var args = arguments;
+    var pos = 1;
+    return string.replace(/%(.)/g, function(_, chr) {
+        if (chr === 's') return args[pos++];
+        if (chr === 'd') return Number(args[pos++]);
+        return chr;
+    });
+}
+
+command.prototype.image = function(project, callback) {
     var mapnik = require('mapnik');
     var sm = new (require('sphericalmercator'))();
     var map = new mapnik.Map(this.opts.width, this.opts.height);
@@ -212,126 +362,177 @@ command.prototype.pdf = function(project, callback) {
         strict: false,
         base: path.join(this.opts.files, 'project', project.id) + '/'
     });
-    map.bufferSize = this.opts.bufferSize;
     map.extent = sm.convert(project.mml.bounds, '900913');
     try {
-        map.renderFileSync(this.opts.filepath, { format: this.opts.format });
+        map.renderFileSync(this.opts.filepath, {
+            format: this.opts.format,
+            scale: project.mml.scale
+        });
         callback();
     } catch(err) {
         callback(err);
     }
 };
 
-command.prototype.mbtiles = function (project, callback) {
+command.prototype.tilelive = function (project, callback) {
     var cmd = this;
     var tilelive = require('tilelive');
-    require('mbtiles').registerProtocols(tilelive);
+
+    // Attempt to support additional tilelive protocols.
+    try { require('tilelive-' + this.opts.format).registerProtocols(tilelive); }
+    catch(err) {}
+    try { require(this.opts.format).registerProtocols(tilelive); }
+    catch(err) {}
+
     require('tilelive-mapnik').registerProtocols(tilelive);
 
-    var uri = {
-        protocol: 'mapnik:',
-        slashes: true,
-        xml: project.xml,
-        mml: project.mml,
-        pathname: path.join(this.opts.files, 'project', project.id, project.id + '.xml'),
-        query: { bufferSize: this.opts.bufferSize }
-    };
-    var source;
-    var sink;
+    var opts = this.opts;
 
-    Step(function() {
-        tilelive.load(uri, this);
-    }, function(err, s) {
-        if (err) throw err;
-        source = s;
-        var uri = {protocol:'mbtiles:',pathname:cmd.opts.filepath};
-        tilelive.load(uri, this);
-    }, function(err, s) {
-        if (err) throw err;
-        sink = s;
-        sink.startWriting(this);
-    }, function(err) {
-        if (err) throw err;
-        sink.putInfo(project.mml, this);
-    }, function(err) {
-        if (err) return cmd.error(err, function() {
-            process.exit(1);
-        });
+    // Try to load a job file if one was given and it exists.
+    if (opts.job) {
+        opts.job = path.resolve(opts.job);
+        try {
+            var job = fs.readFileSync(opts.job, 'utf8');
+        } catch(err) {
+            if (err.code !== 'EBADF') throw err;
+        }
+    } else {
+        // Generate a job file based on the output filename.
+        var slug = path.basename(opts.filepath, path.extname(opts.filepath));
+        opts.job = path.join(path.dirname(opts.filepath), slug + '.export');
+    }
 
-        var copy = tilelive.copy({
-            source: source,
-            sink: sink,
+    if (job) {
+        job = JSON.parse(job);
+        if (!cmd.opts.quiet) console.warn('Continuing job ' + opts.job);
+        var scheme = tilelive.Scheme.unserialize(job.scheme);
+        var task = new tilelive.CopyTask(job.from, job.to, scheme, opts.job);
+    } else {
+        if (!cmd.opts.quiet) console.warn('Creating new job ' + opts.job);
+
+        var from = {
+            protocol: 'mapnik:',
+            slashes: true,
+            xml: project.xml,
+            mml: project.mml,
+            pathname: path.join(opts.files, 'project', project.id, project.id + '.xml'),
+            query: {
+                metatile: project.mml.metatile,
+                scale: project.mml.scale
+            }
+        };
+
+        var to = {
+            protocol: opts.format + ':',
+            pathname: opts.filepath,
+            query: { batch: 100 }
+        };
+
+        var scheme = tilelive.Scheme.create(opts.scheme, {
+            list: opts.list,
             bbox: project.mml.bounds,
-            minZoom: project.mml.minzoom,
-            maxZoom: project.mml.maxzoom,
-            concurrency: 100,
-            tiles: true,
-            grids: !!project.mml.interactivity
+            minzoom: project.mml.minzoom,
+            maxzoom: project.mml.maxzoom,
+            metatile: project.mml.metatile,
+            concurrency: Math.floor(
+                Math.pow(project.mml.metatile, 2) * // # of tiles in each metatile
+                require('os').cpus().length *       // expect one metatile to occupy each core
+                4 / cmd.opts.concurrency            // overcommit x4 throttle by export concurrency
+            )
         });
+        var task = new tilelive.CopyTask(from, to, scheme, opts.job);
+    }
 
-        var done = false;
-        var timeout = setInterval(function progress() {
-            var progress = (copy.copied + copy.failed) / copy.total;
-            cmd.put({
-                status: progress < 1 ? 'processing' : 'complete',
-                progress: progress,
-                remaining: cmd.remaining(progress, copy.started),
-                updated: +new Date(),
-            });
-        }, 5000);
 
-        copy.on('warning', function(err) {
-            console.log(err);
+    var errorfile = path.join(path.dirname(opts.job), path.basename(opts.job) + '-failed');
+    if (!cmd.opts.quiet) console.warn('Writing errors to ' + errorfile);
+
+    fs.open(errorfile, 'a', function(err, fd) {
+        if (err) throw err;
+
+        task.on('error', function(err, tile) {
+            console.warn('\r\033[K' + tile.toString() + ': ' + err.message);
+            fs.write(fd, JSON.stringify(tile) + '\n');
+            report(task.stats.snapshot());
         });
-        copy.on('finished', function() {
-            clearInterval(timeout);
+
+        task.on('progress', report);
+
+        task.on('finished', function() {
+            if (!cmd.opts.quiet) console.warn('\nfinished');
             callback();
         });
-        copy.on('error', function(err) {
-            clearInterval(timeout);
-            callback(err);
+
+        task.start(function(err) {
+            if (err) throw err;
+            task.sink.putInfo(project.mml, function(err) {
+                if (err) throw err;
+            });
         });
     });
+
+    function report(stats) {
+        var progress = stats.processed / stats.total;
+        var remaining = cmd.remaining(progress, task.started);
+        cmd.put({
+            status: 'processing',
+            progress: progress,
+            remaining: remaining,
+            updated: +new Date(),
+            rate: stats.speed
+        });
+
+        if (!cmd.opts.quiet) {
+            util.print(formatString('\r\033[K[%s] %s%% %s/%s @ %s/s | %s left | ✓ %s ■ %s □ %s fail %s',
+                formatDuration(stats.date - task.started),
+                ((progress || 0) * 100).toFixed(4),
+                formatNumber(stats.processed),
+                formatNumber(stats.total),
+                formatNumber(stats.speed),
+                formatDuration(remaining),
+                formatNumber(stats.unique),
+                formatNumber(stats.duplicate),
+                formatNumber(stats.skipped),
+                formatNumber(stats.failed)
+            ));
+        }
+    }
 };
 
 command.prototype.upload = function (callback) {
+    if (!this.opts.syncAccount || !this.opts.syncAccessToken)
+        return callback(new Error('MapBox Hosting account must be authorized.'));
+
     var cmd = this;
     var key;
     var bucket;
-    var mapURL = '';
-    var freeURL = '';
-    var modelURL = '';
+    var proxy = Bones.plugin.config.httpProxy || process.env.HTTP_PROXY;
+    var mapURL = _('<%=base%>/<%=account%>/map/<%=handle%>')
+        .template({
+            base: this.opts.syncURL,
+            account: this.opts.syncAccount,
+            handle: this.opts.project
+        });
+    var modelURL = _('<%=base%>/api/Map/<%=account%>.<%=handle%>?access_token=<%=token%>')
+        .template({
+            base: this.opts.syncURL,
+            account: this.opts.syncAccount,
+            handle: this.opts.project,
+            token: this.opts.syncAccessToken
+        });
     var hash = crypto.createHash('md5')
         .update(+new Date + '')
         .digest('hex')
         .substring(0, 6);
-    var policyEndpoint = url.format({
-        protocol: 'http:',
-        host: this.opts.mapboxHost || 'api.tiles.mapbox.com',
-        pathname: '/v2/'+ hash + '/upload.json'
-    });
-
-    // Set URLs for account uploads.
-    if (this.opts.syncAccount && this.opts.syncAccessToken) {
-        mapURL = _('<%=base%>/<%=account%>/map/<%=handle%>')
-            .template({
-                base: this.opts.syncURL,
-                account: this.opts.syncAccount,
-                handle: this.opts.project
-            });
-        modelURL = _('<%=base%>/api/Map/<%=account%>.<%=handle%>?access_token=<%=token%>')
-            .template({
-                base: this.opts.syncURL,
-                account: this.opts.syncAccount,
-                handle: this.opts.project,
-                token: this.opts.syncAccessToken
-            });
-    }
+    var policyEndpoint = url.format(_(url.parse(this.opts.syncAPI)).extend({
+            pathname: '/v2/'+ hash + '/upload.json'
+        }));
 
     Step(function() {
         request.get({
             uri: policyEndpoint,
-            headers: { 'Host': url.parse(policyEndpoint).host }
+            headers: { 'Host': url.parse(policyEndpoint).host },
+            proxy: proxy
         }, this);
     }, function(err, resp, body) {
         if (err) throw err;
@@ -362,16 +563,29 @@ command.prototype.upload = function (callback) {
             .join(''));
         var terminate = new Buffer('\r\n--' + boundary + '--', 'ascii');
 
-        var dest = http.request({
-            host: bucket + '.s3.amazonaws.com',
-            path: '/',
+        var opts = {
             method: 'POST',
             headers: {
                 'Content-Type': 'multipart/form-data; boundary=' + boundary,
                 'Content-Length': stat.size + multipartBody.length + terminate.length,
                 'X_FILE_NAME': filename
             }
-        });
+        };
+        if (proxy) {
+            var parsed = url.parse(proxy);
+            opts.host = parsed.hostname;
+            opts.port = parsed.port;
+            opts.path = 'http://' + bucket + '.s3.amazonaws.com';
+            opts.headers.Host = bucket + '.s3.amazonaws.com';
+            if (parsed.auth) {
+                opts.headers['proxy-authorization'] = 'Basic ' + new Buffer(parsed.auth).toString('base64')
+            }
+        } else {
+            opts.host = bucket + '.s3.amazonaws.com';
+            opts.path = '/';
+        }
+        var dest = http.request(opts);
+
         dest.on('response', function(resp) {
             var data = '';
             var callback = function(err) {
@@ -391,7 +605,6 @@ command.prototype.upload = function (callback) {
                         message += ' (' + parsed.code[0] + ' - ' + parsed.message[0] + ')';
                     return this(new Error(message));
                 }
-                freeURL = resp.headers.location.split('?')[0];
                 this();
             }.bind(this);
             resp.on('data', function(chunk) { chunk += data; });
@@ -416,7 +629,7 @@ command.prototype.upload = function (callback) {
                 updated = Date.now();
                 cmd.put({
                     progress: progress,
-                    status: progress < 1 ? 'processing' : 'complete',
+                    status: 'processing',
                     remaining: cmd.remaining(progress, started),
                     updated: updated
                 });
@@ -428,12 +641,12 @@ command.prototype.upload = function (callback) {
             .pipe(dest, {end: false});
     }, function(err) {
         if (err) throw err;
-        if (!modelURL) return this(); // Free
-
-        request.get(modelURL, this);
+        request.get({
+            uri: modelURL,
+            proxy: proxy
+        }, this);
     }, function(err, res, body) {
         if (err) throw err;
-        if (!modelURL) return this(); // Free
 
         // Let Step catch thrown errors here.
         var model = _(res.statusCode === 404 ? {} : JSON.parse(body)).extend({
@@ -443,13 +656,26 @@ command.prototype.upload = function (callback) {
             status: 'pending',
             url: 'http://' + bucket + '.s3.amazonaws.com/' + key
         });
-        request.put({ url:modelURL, json:model }, this);
+        request.put({
+            url: modelURL,
+            json: model,
+            proxy: proxy
+        }, this);
     }, function(err, res, body) {
-        if (err)
+        console.log('MapBox Hosting account response: ' + util.inspect(body));
+        if (err) {
             return callback(err);
-        if (modelURL && res.statusCode !== 200)
-            return callback(new Error('Map publish failed: ' + res.statusCode));
-        callback(null, { url:modelURL ? mapURL : freeURL });
+        }
+        if (modelURL && res.statusCode !== 200) {
+            var msg;
+            if (body && body.message != undefined) {
+                msg = body.message;
+            } else {
+                msg = 'Map publish failed: ' + res.statusCode;
+            }
+            return callback(new Error(msg));
+        }
+        callback(null, { url:mapURL });
     });
 };
 
@@ -466,8 +692,9 @@ command.prototype.sync = function (project, callback) {
         id: project.id,
         time: + new Date
     });
+    cmd.opts.format = 'mbtiles';
     Step(function() {
-        cmd.mbtiles(project, this);
+        cmd.tilelive(project, this);
     }, function(err) {
         if (err) throw err;
         modifier = 0.5;
diff --git a/commands/global.bones b/commands/global.bones
index 72697ab..bb56b3b 100644
--- a/commands/global.bones
+++ b/commands/global.bones
@@ -5,13 +5,13 @@ var defaults = models.Config.defaults;
 Bones.Command.options['files'] = {
     'title': 'files=[path]',
     'description': 'Path to files directory.',
-    'default': defaults.files.replace('~', process.env.HOME)
+    'default': defaults.files.replace(/^~/, process.env.HOME)
 };
 
-Bones.Command.options['bufferSize'] = {
-    'title': 'bufferSize=[number]',
-    'description': 'Mapnik render buffer size.',
-    'default': defaults.bufferSize
+Bones.Command.options['syncAPI'] = {
+    'title': 'syncAPI=[URL]',
+    'description': 'MapBox API URL.',
+    'default': defaults.syncAPI || ''
 };
 
 Bones.Command.options['syncURL'] = {
@@ -32,5 +32,11 @@ Bones.Command.options['syncAccessToken'] = {
     'default': defaults.syncAccessToken || ''
 };
 
+Bones.Command.options['verbose'] = {
+    'title': 'verbose=on|off',
+    'description': 'verbose logging',
+    'default': defaults.verbose
+};
+
 // Host option is unused.
 delete Bones.Command.options.host;
diff --git a/commands/start.bones b/commands/start.bones
index a52b10c..d104c2b 100644
--- a/commands/start.bones
+++ b/commands/start.bones
@@ -1,7 +1,12 @@
 var path = require('path');
 var spawn = require('child_process').spawn;
+var redirect = require('../lib/redirect.js');
 var defaults = models.Config.defaults;
 var command = commands['start'];
+var crashutil = require('../lib/crashutil');
+// we can drop this when we drop support for ubuntu lucid/maverick/natty
+// https://github.com/mapbox/tilemill/issues/1244
+var ubuntu_gui_workaround = require('../lib/ubuntu_gui_workaround');
 
 command.options['server'] = {
     'title': 'server=1|0',
@@ -32,12 +37,38 @@ command.prototype.initialize = function(plugin, callback) {
     plugin.config.coreUrl = plugin.config.coreUrl ||
         'localhost:' + plugin.config.port;
 
+    // Set proxy env variable before spawning children
+    if (plugin.config.httpProxy) process.env.HTTP_PROXY = plugin.config.httpProxy;
+
+    // munge verbose setting into what bones/millstone expects
+    if (plugin.config.verbose == 'on') {
+        process.env.NODE_ENV = 'development';
+    } else  {
+        // beware, do not set to 'production': https://github.com/mapbox/tilemill/issues/1697
+        process.env.NODE_ENV = 'normal'; // NOTE: normal is arbitrary, just needs to be not 'development'
+    }
+
     Bones.plugin.command = this;
     Bones.plugin.children = {};
     process.title = 'tilemill';
     // Kill child processes on exit.
-    process.on('exit', function() {
-        _(Bones.plugin.children).each(function(child) { child.kill(); });
+    process.on('exit', function(code, signal) {
+        _(Bones.plugin.children).each(function(child, key) {
+            console.warn('[tilemill] Closing child process: ' + key  + " (pid:" + child.pid + ")");
+            child.kill();
+        });
+        if (code !== 0)
+        {
+            crashutil.display_crash_log(function(err,logname) {
+                if (err) {
+                    console.warn('Fatal error: ' + err.stack || err.toString());
+                }
+                if (logname) {
+                    console.warn("[tilemill] Please post this crash log: '" + logname + "' to https://github.com/mapbox/tilemill/issues");
+                }
+            });
+        }
+        console.warn('Exiting [' + process.title + ']');
     });
     // Handle SIGUSR2 for dev integration with nodemon.
     process.once('SIGUSR2', function() {
@@ -49,7 +80,8 @@ command.prototype.initialize = function(plugin, callback) {
 
     if (!plugin.config.server) plugin.children['core'].stderr.on('data', function(d) {
         if (!d.toString().match(/Started \[Server Core:\d+\]./)) return;
-        var client = require('topcube')({
+        var client;
+        var options = {
             url: 'http://' + plugin.config.coreUrl,
             name: 'TileMill',
             width: 800,
@@ -57,13 +89,21 @@ command.prototype.initialize = function(plugin, callback) {
             minwidth: 800,
             minheight: 400,
             // win32-only options.
-            ico: path.resolve(path.join(__dirname + '/../platforms/windows/tilemill.ico')),
-            'cache-path': path.join(process.env.HOME, '.tilemill/cache-cefclient')
+            ico: path.resolve(path.join(__dirname + '/../tilemill.ico')),
+            'cache-path': path.join(process.env.HOME, '.tilemill/cache-cefclient'),
+            'log-file': path.join(process.env.HOME, '.tilemill/cefclient.log')
+        };
+        ubuntu_gui_workaround.check(function(needed) {
+            if (needed) {
+                client = ubuntu_gui_workaround.get_client(options);
+            } else {
+                client = require('topcube')(options);
+            }
+            if (client) {
+                console.warn('[tilemill] Client window created (pass --server=true to disable this)');
+                plugin.children['client'] = client;
+            }
         });
-        if (client) {
-            console.warn('Client window created.');
-            plugin.children['client'] = client;
-        }
     });
 
     callback && callback();
@@ -74,10 +114,29 @@ command.prototype.child = function(name) {
         path.resolve(path.join(__dirname + '/../index.js')),
         name
     ].concat(args));
-    Bones.plugin.children[name].stdout.pipe(process.stdout);
-    Bones.plugin.children[name].stderr.pipe(process.stderr);
+
+    redirect.onData(Bones.plugin.children[name]);
     Bones.plugin.children[name].once('exit', function(code, signal) {
-        if (code === 0) this.child(name);
+        if (code === 0) {
+            // restart server if exit was clean
+            console.warn('[tilemill] Restarting child process: "' + name + '"');
+            this.child(name);
+        } else {
+            if (signal) {
+                var msg = '[tilemill] Error: child process: "' + name + '" failed with signal "' + signal + '"';
+                if (code != undefined)
+                    msg += " and code '" + code + "'";
+                console.warn(msg);
+                _(Bones.plugin.children).each(function(child) { child.kill(signal); });
+                process.exit(1);
+            } else {
+                // Note: it would be great, in many cases, to auto-restart here
+                // but we cannot because we will trigger recursion like in cases
+                // of failed startup due to EADDRINUSE
+                console.warn('[tilemill] Error: child process: "' + name + '" failed with code "' + code + '"')
+                _(Bones.plugin.children).each(function(child) { child.kill(); });
+            }
+        }
     }.bind(this));
 };
 
diff --git a/commands/tile.bones b/commands/tile.bones
index 150e285..c048e4f 100644
--- a/commands/tile.bones
+++ b/commands/tile.bones
@@ -13,7 +13,7 @@ command.options['tilePort'] = {
 
 command.prototype.bootstrap = function(plugin, callback) {
     var settings = Bones.plugin.config;
-    settings.files = path.resolve(settings.files);
+    settings.files = path.resolve(settings.files.replace(/^~/, process.env.HOME));
     process.title = 'tilemill-tile';
     callback();
 };
diff --git a/controllers/Router.bones b/controllers/Router.bones
index 0994278..12137ff 100644
--- a/controllers/Router.bones
+++ b/controllers/Router.bones
@@ -5,39 +5,46 @@ controller.prototype.initialize = function() {
     new views.App({ el: $('body') });
 
     // Check whether there is a new version of TileMill or not.
-    (new models.Config).fetch({success: function(m) {
-        if (window.abilities.platform === 'darwin') return;
-        if (!m.get('updates')) return;
-        if (!semver.gt(m.get('updatesVersion'),
-            window.abilities.tilemill.version)) return;
-        new views.Modal({
-            content:_('\
-                A new version of TileMill is available.<br />\
-                Update to TileMill <%=version%> today.<br/>\
-                <small>You can disable update notifications in the <strong>Settings</strong> panel.</small>\
-            ').template({ version:m.get('updatesVersion') }),
-            affirmative: 'Update',
-            negative: 'Later',
-            callback: function() { window.open('http://tilemill.com') }
-        });
-    }});
+    $.ajax({
+        url: '/api/updatesVersion',
+        type: 'GET',
+        dataType: 'json',
+        success: function(data) {
+            if (window.abilities.platform === 'darwin') return;
+            if (!data.updates) return;
+            if (!semver.gt(data.updatesVersion,
+                    window.abilities.tilemill.version)) return;
+            new views.Modal({
+                content:_('\
+                            A new version of TileMill is available.<br />\
+                            Update to TileMill <%=version%> today.<br/>\
+                            <small>You can disable update notifications in the <strong>Settings</strong> panel.</small>\
+                            ').template({ version:data.updatesVersion }),
+                    affirmative: 'Update',
+                negative: 'Later',
+                callback: function() { window.open('http://tilemill.com') }
+            });
+        }
+    });
 
-    // Add catchall routes for wrapper goto's, error page.
-    this.route(/[^?]*\?goto=(.*)/, 'goto', this.goto);
+    // Add catchall routes for error page.
     this.route(/^(.*?)/, 'error', this.error);
 };
 
 controller.prototype.routes = {
+    '.*\?goto=*path': 'goto',
     '': 'projects',
     '/': 'projects',
     '/project/:id': 'project',
     '/project/:id/export': 'projectExport',
     '/project/:id/export/:format': 'projectExport',
     '/project/:id/settings': 'projectSettings',
+    '/oauth/success': 'oauthSuccess',
+    '/oauth/error': 'oauthError',
     '/manual': 'manual',
     '/manual/:page?': 'manual',
     '/settings': 'config',
-    '/plugins': 'plugins'
+    '/plugins': 'plugins',
 };
 
 controller.prototype.goto = function(path) {
@@ -49,13 +56,14 @@ controller.prototype.error = function() {
     new views.Error(new Error('Page not found.'));
 };
 
-controller.prototype.projects = function() {
+controller.prototype.projects = function(next) {
     (new models.Projects()).fetch({
         success: function(collection) {
             new views.Projects({
                 el: $('#page'),
                 collection: collection
             });
+            if (next) next();
         },
         error: function(m, e) { new views.Modal(e); }
     });
@@ -126,3 +134,23 @@ controller.prototype.plugins = function() {
     });
 };
 
+controller.prototype.oauthSuccess = function() {
+    this.projects(function() {
+        new views.Modal({
+            content: 'Your MapBox account was authorized successfully.',
+            negative: '',
+            callback: function() {}
+        });
+    });
+};
+
+controller.prototype.oauthError = function() {
+    this.projects(function() {
+        new views.Modal({
+            content: 'An error occurred while authorizing your MapBox account.',
+            negative: '',
+            callback: function() {}
+        });
+    });
+};
+
diff --git a/examples/open-streets-dc/.thumb.png b/examples/open-streets-dc/.thumb.png
index 8e713be..dc9eb4a 100644
Binary files a/examples/open-streets-dc/.thumb.png and b/examples/open-streets-dc/.thumb.png differ
diff --git a/examples/open-streets-dc/highway.mss b/examples/open-streets-dc/highway.mss
index d997393..66e3dc1 100644
--- a/examples/open-streets-dc/highway.mss
+++ b/examples/open-streets-dc/highway.mss
@@ -1,443 +1,249 @@
-/* ---- PALETTE ---- */
-
- at motorway: #F8D6E0; /* #90BFE0 */
- at trunk: #FFFABB;
- at primary: @trunk;
- at secondary: @trunk;
- at road: #bbb;
- at track: @road;
- at footway: #6B9;
- at cycleway: #69B;
-
-/* ---- ROAD COLORS ---- */
-
-/*.highway.line { line-color: #f00; } /* debug */
-
-.highway[TYPE='motorway'] {
-  .line[zoom>=7]  { 
-    line-color:spin(darken(@motorway,36),-10);
-    line-cap:round;
-    line-join:round;
-  }
-  .fill[zoom>=10] {
+ at motorway:  #F56544;
+ at trunk:     @motorway;
+ at primary:   #FFC53C;
+ at secondary: @primary;
+ at road:      #ccc;
+ at track:     @road;
+ at footway:   #ac9;
+ at cycleway:  #9ca;
+
+#roads::line {
+  [zoom>=8][zoom<=12] {
+    [type='motorway'] { line-color:@motorway; }
+    [type='trunk'] { line-color:@trunk; }
+    [type='motorway'],
+    [type='trunk'] {
+      line-cap:round;
+      line-join:round;
+      [zoom=11] { line-width:2; }
+    }
+  }
+  [zoom=11] {
+    [type='primary'] { line-color:@primary; }
+    [type='secondary'] { line-color:@secondary; }
+    [type='primary'],
+    [type='secondary'] {
+      line-cap:round;
+      line-join:round;
+      [zoom=11] { line-width:1.5; }
+    }
+  }
+  [zoom>=12][zoom<=13] {
+    [type='motorway_link'],
+    [type='trunk_link'],
+    [type='primary_link'],
+    [type='secondary_link'],
+    [type='tertiary'],
+    [type='tertiary_link'],
+    [type='unclassified'],
+    [type='residential'],
+    [type='living_street'] {
+      line-color:@road;
+      [zoom=12] { line-width:0.5; }
+    }
+  }
+  [zoom>=14][zoom<=15] {
+    [type='service'],
+    [type='pedestrian'] {
+      line-color:@road;
+      [zoom=14] { line-width:0.5; }
+    }
+  }
+  [zoom>=14] {
+    [type='track'],
+    [type='footway'],
+    [type='bridleway'] {
+      line-color:@footway;
+      line-dasharray:4,1;
+      line-cap:butt;
+      [zoom=16] { line-width:1.2; }
+      [zoom=17] { line-width:1.6; }
+      [zoom>17] { line-width:2; }
+    }
+    [type='steps'] {
+      line-color:@footway;
+      line-dasharray:2,2;
+      line-cap:butt;
+      [zoom=16] { line-width:2; }
+      [zoom=17] { line-width:3; }
+      [zoom>17] { line-width:4; }
+    }
+    [type='cycleway'] {
+      line-color: @cycleway;
+      line-dasharray:4,1;
+      line-cap:butt;
+      [zoom=16] { line-width:1.2; }
+      [zoom=17] { line-width:1.6; }
+      [zoom>17] { line-width:2; }
+    }
+  }
+}
+
+#motorways::case[zoom>=6][zoom<=12],
+#mainroads::case[zoom>=10][zoom<=12],
+#roads::case[zoom>=13][tunnel!=1][bridge!=1],
+#tunnels::case[zoom>=13][tunnel=1],
+#bridges::case[zoom>=13][bridge=1] {
+  // -- line style --
+  line-cap:round;
+  line-join:round;
+  line-width:0;
+  [tunnel=1] {
+    line-cap:butt;
+    line-dasharray:6,3;
+  }
+  [bridge=1] { line-color:@road * 0.8; }
+  // -- colors --
+  line-color:@road;
+  [type='motorway'],
+  [type='motorway_link'] {
     line-color:@motorway;
-    line-cap:round;
-    line-join:round;
+    [bridge=1] { line-color:@motorway * 0.8; }
   }
-}
-
-.highway[TYPE='motorway_link'] {
-  .line[zoom>=7]  { 
-    line-color:spin(darken(@motorway,36),-10);
-    line-cap:round;
-    line-join:round;
-  }
-  .fill[zoom>=12] {
-    line-color:@motorway;
-    line-cap:round;
-    line-join:round;
-  }
-}
-
-.highway[TYPE='trunk'],
-.highway[TYPE='trunk_link'] {
-  .line[zoom>=7] {
-    line-color:spin(darken(@trunk,36),-10);
-    line-cap:round;
-    line-join:round;
-  }
-  .fill[zoom>=11] {
+  [type='trunk'],
+  [type='trunk_link'] {
     line-color:@trunk;
-    line-cap:round;
-    line-join:round;
+    [bridge=1] { line-color:@trunk * 0.8; }
   }
-}
-
-.highway[TYPE='primary'],
-.highway[TYPE='primary_link'] {
-  .line[zoom>=7] {
-    line-color:spin(darken(@primary,36),-10);
-    line-cap:round;
-    line-join:round;
-  }
-  .fill[zoom>=12] {
+  [type='primary'],
+  [type='primary_link'] {
     line-color:@primary;
-    line-cap:round;
-    line-join:round;
-  }
-}
-
-.highway[TYPE='secondary'] {
-  .line[zoom>=8] {
-    line-color:spin(darken(@secondary,36),-10);
-    line-cap:round;
-    line-join:round;
+    [bridge=1] { line-color:@primary * 0.8; }
   }
-  .fill[zoom>=12] {
+  [type='secondary'],
+  [type='secondary_link'] {
     line-color:@secondary;
-    line-cap:round;
-    line-join:round;
-  }
-}
-
-.highway[TYPE='secondary_link'] {
-  .line[zoom>=12] {
-    line-color:spin(darken(@secondary,36),-10);
-    line-cap:round;
-    line-join:round;
-  }
-  .fill[zoom>=14] {
-    line-color:@secondary;
-    line-cap:round;
-    line-join:round;
-  }
-}
-
-.highway[TYPE='living_street'],
-.highway[TYPE='residential'],
-.highway[TYPE='road'],
-.highway[TYPE='tertiary'],
-.highway[TYPE='unclassified'] {
-  .line[zoom>=10] {
-    line-color:@road;
-    line-cap:round;
-    line-join:round;
-  }
-  .fill[zoom>=14] {
-    line-color:#fff;
-    line-cap:round;
-    line-join:round;
-  }
-}
-
-.highway[TYPE='service'] {
-  .line[zoom>=13] {
-    line-color:@road;
-    line-cap:round;
-    line-join:round;
-  }
-  .fill[zoom>=16] {
-    line-color:#fff;
-    line-cap:round;
-    line-join:round;
-  }
-}
-
-.highway[TYPE='track'] {
-  .line[zoom>=13] {
-    line-color:@track;
-    line-cap:round;
-    line-join:round;
-  }
-}
-
-.highway[TYPE='footway'],
-.highway[TYPE='path'],
-.highway[TYPE='pedestrian'] {
-  .line[zoom>=14] {
-    line-color:@footway;
-    line-cap:round;
-    line-join:round;
-  }
-}
-
-.highway[TYPE='cycleway'] {
-  .line[zoom>=14] {
-    line-color:@cycleway;
-    line-cap:round;
-    line-join:round;
-  }
-}
-
-/* ---- ROAD WIDTHS ---- */
-
-.highway[zoom=7] {
-  .line[TYPE='motorway'] { line-width: 1.0; }
-  .line[TYPE='trunk']    { line-width: 0.8; }
-  .line[TYPE='primary']  { line-width: 0.6; }
-}
-
-.highway[zoom=8] {
-  .line[TYPE='motorway'] { line-width: 1.0; }
-  .line[TYPE='trunk']    { line-width: 0.8; }
-  .line[TYPE='primary']  { line-width: 0.5; }
-  .line[TYPE='secondary']{ line-width: 0.3; }
-}
-
-.highway[zoom=9] {
-  .line[TYPE='motorway'] { line-width: 1.0; }
-  .line[TYPE='trunk']    { line-width: 0.8; }
-  .line[TYPE='primary']  { line-width: 0.6; }
-  .line[TYPE='secondary']{ line-width: 0.4; }
-}
-
-.highway[zoom=10] {
-  .line[TYPE='motorway'] { line-width: 0.8 + 1.6; }
-  .fill[TYPE='motorway'] { line-width: 0.8; }
-  
-  .line[TYPE='trunk']    { line-width: 1.4; }
-  
-  .line[TYPE='primary']  { line-width: 1.2; }
-  
-  .line[TYPE='secondary']{ line-width: 0.8; }
-  
-  .line[TYPE='living_street'],
-  .line[TYPE='residential'],
-  .line[TYPE='road'],
-  .line[TYPE='tertiary'],
-  .line[TYPE='unclassified'] { line-width: 0.2; }
-}
-
-.highway[zoom=11] {
-  .line[TYPE='motorway']      { line-width: 1.0 + 1.8; }
-  .fill[TYPE='motorway']      { line-width: 1.0; }
-  .line[TYPE='trunk']         { line-width: 0.8 + 1.6; }
-  .fill[TYPE='trunk']         { line-width: 0.8; }
-  .line[TYPE='primary']       { line-width: 1.4; }
-  .line[TYPE='secondary']     { line-width: 1.0; }
-  
-  .line[TYPE='motorway_link'] { line-width: 0.6; }
-  .line[TYPE='trunk_link']    { line-width: 0.5; }
-  .line[TYPE='primary_link']  { line-width: 0.4; }
-  
-  .line[TYPE='living_street'],
-  .line[TYPE='residential'],
-  .line[TYPE='road'],
-  .line[TYPE='tertiary'],
-  .line[TYPE='unclassified'] { line-width: 0.4; }
-}
-
-.highway[zoom=12] {
-  .line[TYPE='motorway']      { line-width: 1.2 + 2; }
-  .fill[TYPE='motorway']      { line-width: 1.2; }
-  .line[TYPE='trunk']         { line-width: 1.0 + 1.8; }
-  .fill[TYPE='trunk']         { line-width: 1.0; }
-  .line[TYPE='primary']       { line-width: 0.8 + 1.6; }
-  .fill[TYPE='primary']       { line-width: 0.8; }
-  .line[TYPE='secondary']     { line-width: 0.8 + 1.6; }
-  .fill[TYPE='secondary']     { line-width: 0.8; }
-  
-  .line[TYPE='motorway_link'] { line-width: 1.0 + 1.8; }
-  .fill[TYPE='motorway_link'] { line-width: 1.0; }
-  .line[TYPE='trunk_link']    { line-width: 0.8 + 1.6; }
-  .fill[TYPE='trunk_link']    { line-width: 0.8; }
-  .line[TYPE='primary_link']  { line-width: 0.8 + 1.6; }
-  .fill[TYPE='primary_link']  { line-width: 0.8; }
-  .line[TYPE='secondary_link']  { line-width: 0.8; }
-  
-  .line[TYPE='living_street'],
-  .line[TYPE='residential'],
-  .line[TYPE='road'],
-  .line[TYPE='tertiary'],
-  .line[TYPE='unclassified']  { line-width: 0.6; }
-}
-
-.highway[zoom=13] {
-  .line[TYPE='motorway']      { line-width: 2.0 + 2; }
-  .fill[TYPE='motorway']      { line-width: 2.0; }
-  .line[TYPE='trunk']         { line-width: 1.4 + 2; }
-  .fill[TYPE='trunk']         { line-width: 1.4; }
-  .line[TYPE='primary']       { line-width: 1.2 + 2; }
-  .fill[TYPE='primary']       { line-width: 1.2; }
-  .line[TYPE='primary_link'],
-  .line[TYPE='secondary']     { line-width: 1.0 + 2; }
-  .fill[TYPE='primary_link'],
-  .fill[TYPE='secondary']     { line-width: 1.0; }
-  
-  .line[TYPE='motorway_link'] { line-width: 1.0 + 2; }
-  .fill[TYPE='motorway_link'] { line-width: 1.0; }
-  .line[TYPE='trunk_link']    { line-width: 1.0 + 2; }
-  .fill[TYPE='trunk_link']    { line-width: 1.0; }
-  .line[TYPE='primary_link']  { line-width: 1.0 + 2; }
-  .fill[TYPE='primary_link']  { line-width: 1.0; }
-  .line[TYPE='secondary_link']{ line-width: 0.8; }
-  
-  .line[TYPE='living_street'],
-  .line[TYPE='residential'],
-  .line[TYPE='road'],
-  .line[TYPE='tertiary'],
-  .line[TYPE='unclassified']  { line-width: 1.0; }
-  .line[TYPE='service']       { line-width: 0.5; }
-  
-  .line[TYPE='track']         { line-width: 0.5; line-dasharray:2,3; }
-}
-
-.highway[zoom=14] {
-  .line[TYPE='motorway']      { line-width: 4 + 2; }
-  .fill[TYPE='motorway']      { line-width: 4; }
-  .line[TYPE='trunk']         { line-width: 3 + 2; }
-  .fill[TYPE='trunk']         { line-width: 3; }
-  .line[TYPE='primary']       { line-width: 2 + 2; }
-  .fill[TYPE='primary']       { line-width: 2; }
-  .line[TYPE='secondary']     { line-width: 2 + 2; }
-  .fill[TYPE='secondary']     { line-width: 2; }
-  
-  .line[TYPE='motorway_link'] { line-width: 1.4 + 2; }
-  .fill[TYPE='motorway_link'] { line-width: 1.4; }
-  .line[TYPE='trunk_link']    { line-width: 1.2 + 2; }
-  .fill[TYPE='trunk_link']    { line-width: 1.2; }
-  .line[TYPE='primary_link']  { line-width: 1.0 + 2; }
-  .fill[TYPE='primary_link']  { line-width: 1.0; }
-  .line[TYPE='secondary_link']{ line-width: 0.8 + 2; }
-  .fill[TYPE='secondary_link']{ line-width: 0.8; }
-  
-  .line[TYPE='living_street'],
-  .line[TYPE='residential'],
-  .line[TYPE='road'],
-  .line[TYPE='tertiary'],
-  .line[TYPE='unclassified']  { line-width: 1.6 + 1.6; }
-  .fill[TYPE='living_street'],
-  .fill[TYPE='residential'],
-  .fill[TYPE='road'],
-  .fill[TYPE='tertiary'],
-  .fill[TYPE='unclassified']  { line-width: 1.6; }
-  .line[TYPE='service']       { line-width: 0.6; }
-  
-  .line[TYPE='track']         { line-width: 0.6; line-dasharray:2,3; }
-  
-  .line[TYPE='cycleway'],
-  .line[TYPE='footway'],
-  .line[TYPE='path'],
-  .line[TYPE='pedestrian'] {
-    line-dasharray:1,2;
-    line-width:0.6;
-  }
-}
-
-.highway[zoom=15] {
-  .line[TYPE='motorway']      { line-width: 6 + 2; }
-  .fill[TYPE='motorway']      { line-width: 6; }
-  .line[TYPE='trunk']         { line-width: 5 + 2; }
-  .fill[TYPE='trunk']         { line-width: 5; }
-  .line[TYPE='primary']       { line-width: 4 + 2; }
-  .fill[TYPE='primary']       { line-width: 4; }
-  .line[TYPE='secondary']     { line-width: 4 + 2; }
-  .fill[TYPE='secondary']     { line-width: 4; }
-  
-  .line[TYPE='motorway_link'] { line-width: 2 + 2; }
-  .fill[TYPE='motorway_link'] { line-width: 2; }
-  .line[TYPE='trunk_link']    { line-width: 1.6 + 2; }
-  .fill[TYPE='trunk_link']    { line-width: 1.6; }
-  .line[TYPE='primary_link']  { line-width: 1.4 + 2; }
-  .fill[TYPE='primary_link']  { line-width: 1.4; }
-  .line[TYPE='secondary_link']{ line-width: 1.0 + 2; }
-  .fill[TYPE='secondary_link']{ line-width: 1.0; }
-  
-  .line[TYPE='living_street'],
-  .line[TYPE='residential'],
-  .line[TYPE='road'],
-  .line[TYPE='tertiary'],
-  .line[TYPE='unclassified']  { line-width: 4 + 2; }
-  .fill[TYPE='living_street'],
-  .fill[TYPE='residential'],
-  .fill[TYPE='road'],
-  .fill[TYPE='tertiary'],
-  .fill[TYPE='unclassified']  { line-width: 4; }
-  .line[TYPE='service']       { line-width: 1; }
-  
-  .line[TYPE='track']         { line-width: 1; line-dasharray:2,3; }
-  
-  .line[TYPE='cycleway'],
-  .line[TYPE='footway'],
-  .line[TYPE='path'],
-  .line[TYPE='pedestrian'] {
-    line-dasharray:1,2;
-    line-width:0.8;
-  }
-}
-
-.highway[zoom=16] {
-  .line[TYPE='motorway']      { line-width: 9 + 3; }
-  .fill[TYPE='motorway']      { line-width: 9; }
-  .line[TYPE='trunk']         { line-width: 8 + 2.5; }
-  .fill[TYPE='trunk']         { line-width: 8; }
-  .line[TYPE='primary']       { line-width: 7 + 2; }
-  .fill[TYPE='primary']       { line-width: 7; }
-  .line[TYPE='secondary']     { line-width: 6 + 2; }
-  .fill[TYPE='secondary']     { line-width: 6; }
-  
-  .line[TYPE='motorway_link'] { line-width: 3 + 2.5; }
-  .fill[TYPE='motorway_link'] { line-width: 3; }
-  .line[TYPE='trunk_link']    { line-width: 2 + 2; }
-  .fill[TYPE='trunk_link']    { line-width: 2; }
-  .line[TYPE='primary_link']  { line-width: 1.8 + 2; }
-  .fill[TYPE='primary_link']  { line-width: 1.8; }
-  .line[TYPE='secondary_link']{ line-width: 1.4 + 2; }
-  .fill[TYPE='secondary_link']{ line-width: 1.4; }
-  
-  .line[TYPE='living_street'],
-  .line[TYPE='residential'],
-  .line[TYPE='road'],
-  .line[TYPE='tertiary'],
-  .line[TYPE='unclassified']  { line-width: 6 + 2; }
-  .fill[TYPE='living_street'],
-  .fill[TYPE='residential'],
-  .fill[TYPE='road'],
-  .fill[TYPE='tertiary'],
-  .fill[TYPE='unclassified']  { line-width: 6; }
-  .line[TYPE='service']       { line-width: 1.4 + 2; }
-  .fill[TYPE='service']       { line-width: 1.4; }
-  
-  .line[TYPE='track']         { line-width: 1.2; line-dasharray:2,3; }
-  
-  .line[TYPE='cycleway'],
-  .line[TYPE='footway'],
-  .line[TYPE='path'],
-  .line[TYPE='pedestrian'] {
-    line-dasharray:1,2;
-    line-width:1.0;
-  }
-}
-
-.highway[zoom>=17] {
-  .line[TYPE='motorway']      { line-width: 13 + 3; }
-  .fill[TYPE='motorway']      { line-width: 13; }
-  .line[TYPE='trunk']         { line-width: 10 + 2.5; }
-  .fill[TYPE='trunk']         { line-width: 10; }
-  .line[TYPE='primary']       { line-width: 9 + 2; }
-  .fill[TYPE='primary']       { line-width: 9; }
-  .line[TYPE='secondary']     { line-width: 8 + 2; }
-  .fill[TYPE='secondary']     { line-width: 8; }
-  
-  .line[TYPE='motorway_link'] { line-width: 4 + 2.5; }
-  .fill[TYPE='motorway_link'] { line-width: 4; }
-  .line[TYPE='trunk_link']    { line-width: 3.5 + 2; }
-  .fill[TYPE='trunk_link']    { line-width: 3.5; }
-  .line[TYPE='primary_link']  { line-width: 3 + 2; }
-  .fill[TYPE='primary_link']  { line-width: 3; }
-  .line[TYPE='secondary_link']{ line-width: 2.5 + 2; }
-  .fill[TYPE='secondary_link']{ line-width: 2.5; }
-  
-  .line[TYPE='living_street'],
-  .line[TYPE='residential'],
-  .line[TYPE='road'],
-  .line[TYPE='tertiary'],
-  .line[TYPE='unclassified']  { line-width: 8 + 2; }
-  .fill[TYPE='living_street'],
-  .fill[TYPE='residential'],
-  .fill[TYPE='road'],
-  .fill[TYPE='tertiary'],
-  .fill[TYPE='unclassified']  { line-width: 8; }
-  
-  .line[TYPE='service']       { line-width: 2 + 2; }
-  .fill[TYPE='service']       { line-width: 2; }
-  
-  .line[TYPE='track']         { line-width: 1.4; line-dasharray:2,3; }
-  
-  .line[TYPE='cycleway'],
-  .line[TYPE='footway'],
-  .line[TYPE='path'],
-  .line[TYPE='pedestrian'] {
-    line-dasharray:2,3;
-    line-width:1.2;
+    [bridge=1] { line-color:@secondary * 0.8; }
+  }
+  // -- widths --
+  [type='motorway'],
+  [type='trunk'] {
+    [zoom=12] { line-width: 1.2 + 2; }
+    [zoom=13] { line-width: 2 + 2; }
+    [zoom=14] { line-width: 4 + 2; }
+    [zoom=15] { line-width: 6 + 2; }
+    [zoom=16] { line-width: 9 + 3; }
+    [zoom=17] { line-width: 13 + 3; }
+    [zoom>17] { line-width: 15 + 3; }
+  }
+  [type='primary'],
+  [type='secondary'] {
+    [zoom=12] { line-width: 1 + 2; }
+    [zoom=13] { line-width: 1.2 + 2; }
+    [zoom=14] { line-width: 2 + 2; }
+    [zoom=15] { line-width: 4 + 2; }
+    [zoom=16] { line-width: 7 + 3; }
+    [zoom=17] { line-width: 9 + 3; }
+    [zoom>17] { line-width: 11 + 3; }
+  }
+  [type='motorway_link'],
+  [type='trunk_link'],
+  [type='primary_link'],
+  [type='secondary_link'],
+  [type='tertiary'],
+  [type='tertiary_link'],
+  [type='unclassified'],
+  [type='residential'],
+  [type='living_street'] {
+    [zoom=14] { line-width: 1.6 + 1.6; }
+    [zoom=15] { line-width: 4 + 2; }
+    [zoom=16] { line-width: 6 + 2; }
+    [zoom=17] { line-width: 8 + 3; }
+    [zoom>17] { line-width: 10 + 3; }
+  }
+  [type='service'],
+  [type='pedestrian'] {
+    [zoom=16] { line-width: 1.6 + 2; }
+    [zoom=17] { line-width: 2 + 2; }
+    [zoom>17] { line-width: 3 + 2.5; }
+  }
+}
+
+#bridges::case[zoom>=13][bridge=1] {
+  line-cap:butt;
+}
+
+#motorways::fill[zoom>=6][zoom<=12],
+#mainroads::fill[zoom>=10][zoom<=12],
+#roads::fill[zoom>=13][tunnel!=1][bridge!=1],
+#tunnels::fill[zoom>=13][tunnel=1],
+#bridges::fill[zoom>=13][bridge=1] {
+  // -- line style --
+  line-cap:round;
+  line-join:round;
+  line-width:0;
+  // -- colors --
+  line-color:lighten(@road,20);
+  [type='motorway'],
+  [type='motorway_link'] {
+    line-color:lighten(@motorway,10);
+    [tunnel=1] { line-color:@motorway * 0.5 + rgb(127,127,127); }
+  }
+  [type='trunk'],
+  [type='trunk_link'] {
+    line-color:lighten(@trunk,10);
+    [tunnel=1] { line-color:@trunk * 0.5 + rgb(127,127,127); }
+  }
+  [type='primary'],
+  [type='primary_link'] {
+    line-color:lighten(@primary,20);
+    [tunnel=1] { line-color:@primary * 0.5 + rgb(127,127,127); }
+  }
+  [type='secondary'],
+  [type='secondary_link'] {
+    line-color:lighten(@secondary,20);
+    [tunnel=1] { line-color:@secondary * 0.5 + rgb(127,127,127); }
+  }
+  // -- widths --
+  [type='motorway'],
+  [type='trunk'] {
+    [zoom=12] { line-width: 1.2; }
+    [zoom=13] { line-width: 2; }
+    [zoom=14] { line-width: 4; }
+    [zoom=15] { line-width: 6; }
+    [zoom=16] { line-width: 9; }
+    [zoom=17] { line-width: 13; }
+    [zoom>17] { line-width: 15; }
+  }
+  [type='primary'],
+  [type='secondary'] {
+    [zoom=12] { line-width: 1; }
+    [zoom=13] { line-width: 1.2; }
+    [zoom=14] { line-width: 2; }
+    [zoom=15] { line-width: 4; }
+    [zoom=16] { line-width: 7; }
+    [zoom=17] { line-width: 9; }
+    [zoom>17] { line-width: 11; }
+  }
+  [type='motorway_link'],
+  [type='trunk_link'],
+  [type='primary_link'],
+  [type='secondary_link'],
+  [type='tertiary'],
+  [type='tertiary_link'],
+  [type='unclassified'],
+  [type='residential'],
+  [type='living_street'] {
+    [zoom=14] { line-width: 1.6; }
+    [zoom=15] { line-width: 4; }
+    [zoom=16] { line-width: 6; }
+    [zoom=17] { line-width: 8; }
+    [zoom>17] { line-width: 10; }
+  }
+  [type='service'],
+  [type='pedestrian'] {
+    [zoom=16] { line-width: 1.6; }
+    [zoom=17] { line-width: 2; }
+    [zoom>17] { line-width: 3; }
   }
 }
 
 /* ---- ONE WAY ARROWS ---- */
 
-.highway.fill::oneway_arrow[zoom>15][ONEWAY='yes'] {
-  marker-type:arrow;
-  marker-width:1;
+#road-label::oneway_arrow[zoom>15][oneway=1] {
+  marker-file:url("shape://arrow");
+  marker-width:15;
+  marker-placement:line;
   marker-line-width:1;
   marker-line-opacity:0.5;
   marker-line-color:#fff;
diff --git a/examples/open-streets-dc/images/water.png b/examples/open-streets-dc/images/water.png
new file mode 100644
index 0000000..12119f8
Binary files /dev/null and b/examples/open-streets-dc/images/water.png differ
diff --git a/examples/open-streets-dc/images/wood.png b/examples/open-streets-dc/images/wood.png
new file mode 100644
index 0000000..a52da8b
Binary files /dev/null and b/examples/open-streets-dc/images/wood.png differ
diff --git a/examples/open-streets-dc/labels.mss b/examples/open-streets-dc/labels.mss
index 32b0c26..330153f 100644
--- a/examples/open-streets-dc/labels.mss
+++ b/examples/open-streets-dc/labels.mss
@@ -2,41 +2,41 @@
 
 /* ---- HIGHWAY ---- */
 
-.highway-label {
+#road-label {
   text-face-name:@font_reg;
   text-halo-radius:1;
   text-placement:line;
   text-name:"''";
-  [TYPE='motorway'][zoom>=12] {
-    text-name:"[NAME]";
-    text-fill:spin(darken(@motorway,70),-15);
-    text-halo-fill:lighten(@motorway,8);
+  [type='motorway'][zoom>=12] {
+    text-name:"[name]";
+    text-fill:spin(darken(@motorway,50),-15);
+    text-halo-fill:lighten(@motorway,15);
     [zoom>=13] { text-size:11; }
     [zoom>=15] { text-size:12; }
   }
-  [TYPE='trunk'][zoom>=12] {
-    text-name:"[NAME]";
-    text-fill:spin(darken(@trunk,70),-15);
-    text-halo-fill:lighten(@trunk,8);
+  [type='trunk'][zoom>=12] {
+    text-name:"[name]";
+    text-fill:spin(darken(@trunk,50),-15);
+    text-halo-fill:lighten(@trunk,15);
     [zoom>=15] { text-size:11; }
   }
-  [TYPE='primary'][zoom>=13] {
-    text-name:"[NAME]";
-    text-fill:spin(darken(@primary,70),-15);
-    text-halo-fill:lighten(@primary,8);
+  [type='primary'][zoom>=13] {
+    text-name:"[name]";
+    text-fill:spin(darken(@primary,50),-15);
+    text-halo-fill:lighten(@primary,15);
     [zoom>=15] { text-size:11; }
   }
-  [TYPE='secondary'][zoom>=13] {
-    text-name:"[NAME]";
-    text-fill:spin(darken(@secondary,70),-15);
-    text-halo-fill:lighten(@secondary,8);
+  [type='secondary'][zoom>=13] {
+    text-name:"[name]";
+    text-fill:spin(darken(@secondary,50),-15);
+    text-halo-fill:lighten(@secondary,15);
     [zoom>=15] { text-size:11; }
   }
-  [TYPE='residential'][zoom>=15],
-  [TYPE='road'][zoom>=15],
-  [TYPE='tertiary'][zoom>=15],
-  [TYPE='unclassified'][zoom>=15] {
-    text-name:"[NAME]";
+  [type='residential'][zoom>=15],
+  [type='road'][zoom>=15],
+  [type='tertiary'][zoom>=15],
+  [type='unclassified'][zoom>=15] {
+    text-name:"[name]";
     text-fill:#444;
     text-halo-fill:#fff;
   }
@@ -44,9 +44,9 @@
 
 /* ---- LOCATION ---- */
 
-.location[PLACE='city'][zoom>6][zoom<14] {
+#places[type='city'][zoom>6][zoom<14] {
   text-face-name:@font_reg;
-  text-name:"[NAME]";
+  text-name:"[name]";
   text-fill:#444;
   text-halo-fill:rgba(255,255,255,0.8);
   text-halo-radius:2;
@@ -65,9 +65,9 @@
   }
 }
 
-.location[PLACE='town'][zoom>6][zoom<15] {
+#places[type='town'][zoom>6][zoom<15] {
   text-face-name:@font_reg;
-  text-name:"[NAME]";
+  text-name:"[name]";
   text-fill:#444;
   text-halo-fill:rgba(255,255,255,0.8);
   text-halo-radius:2;
@@ -91,30 +91,32 @@
   }
 }
 
-.location[PLACE='hamlet'][zoom>14][zoom<18],
-.location[PLACE='suburb'][zoom>14][zoom<18] {
+#places[type='hamlet'][zoom>13][zoom<18],
+#places[type='suburb'][zoom>13][zoom<18],
+#places[type='neighbourhood'][zoom>14][zoom<18] {
   text-face-name:@font_reg;
-  text-name:"[NAME]";
+  text-name:"[name]";
   text-fill:#555;
   text-halo-fill:rgba(255,255,255,0.8);
   text-halo-radius:2;
   text-wrap-width:100;
+  text-wrap-before:true;
   [zoom=15] {
     text-size:11;
-    text-character-spacing:2;
+    text-character-spacing:1;
     text-wrap-width:50;
-    text-line-spacing:2;
+    text-line-spacing:1;
   }
   [zoom=16] {
     text-size:13;
-    text-character-spacing:4;
+    text-character-spacing:2;
     text-wrap-width:80;
-    text-line-spacing:4;
+    text-line-spacing:2;
   }
   [zoom=17] {
     text-size:15;
-    text-character-spacing:8;
+    text-character-spacing:4;
     text-wrap-width:100;
-    text-line-spacing:8;
+    text-line-spacing:4;
   }
 }
diff --git a/examples/open-streets-dc/layers/dc-coastline.dbf b/examples/open-streets-dc/layers/dc-coastline.dbf
new file mode 100644
index 0000000..aa0b3e2
Binary files /dev/null and b/examples/open-streets-dc/layers/dc-coastline.dbf differ
diff --git a/examples/open-streets-dc/layers/dc-coastline.prj b/examples/open-streets-dc/layers/dc-coastline.prj
new file mode 100644
index 0000000..fc5d912
--- /dev/null
+++ b/examples/open-streets-dc/layers/dc-coastline.prj
@@ -0,0 +1 @@
+PROJCS["Popular_Visualisation_CRS_Mercator_deprecated",GEOGCS["GCS_Popular Visualisation CRS",DATUM["D_Popular_Visualisation_Datum",SPHEROID["Popular_Visualisation_Sphere",6378137,0]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Mercator"],PARAMETER["central_meridian",0],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["Meter",1],PARAMETER["standard_parallel_1",0.0]]
\ No newline at end of file
diff --git a/examples/open-streets-dc/layers/dc-coastline.shp b/examples/open-streets-dc/layers/dc-coastline.shp
new file mode 100644
index 0000000..9c76724
Binary files /dev/null and b/examples/open-streets-dc/layers/dc-coastline.shp differ
diff --git a/examples/open-streets-dc/layers/dc-coastline.shx b/examples/open-streets-dc/layers/dc-coastline.shx
new file mode 100644
index 0000000..8634dc0
Binary files /dev/null and b/examples/open-streets-dc/layers/dc-coastline.shx differ
diff --git a/examples/open-streets-dc/layers/dc_osm_highway/dc_osm_highway.dbf b/examples/open-streets-dc/layers/dc_osm_highway/dc_osm_highway.dbf
deleted file mode 100644
index 8092284..0000000
Binary files a/examples/open-streets-dc/layers/dc_osm_highway/dc_osm_highway.dbf and /dev/null differ
diff --git a/examples/open-streets-dc/layers/dc_osm_highway/dc_osm_highway.index b/examples/open-streets-dc/layers/dc_osm_highway/dc_osm_highway.index
deleted file mode 100644
index bf37736..0000000
Binary files a/examples/open-streets-dc/layers/dc_osm_highway/dc_osm_highway.index and /dev/null differ
diff --git a/examples/open-streets-dc/layers/dc_osm_highway/dc_osm_highway.prj b/examples/open-streets-dc/layers/dc_osm_highway/dc_osm_highway.prj
deleted file mode 100644
index 379ef7c..0000000
--- a/examples/open-streets-dc/layers/dc_osm_highway/dc_osm_highway.prj
+++ /dev/null
@@ -1 +0,0 @@
-GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],TOWGS84[0,0,0,0,0,0,0],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]
\ No newline at end of file
diff --git a/examples/open-streets-dc/layers/dc_osm_highway/dc_osm_highway.shp b/examples/open-streets-dc/layers/dc_osm_highway/dc_osm_highway.shp
deleted file mode 100644
index 35032a0..0000000
Binary files a/examples/open-streets-dc/layers/dc_osm_highway/dc_osm_highway.shp and /dev/null differ
diff --git a/examples/open-streets-dc/layers/dc_osm_highway/dc_osm_highway.shx b/examples/open-streets-dc/layers/dc_osm_highway/dc_osm_highway.shx
deleted file mode 100644
index 9555b2c..0000000
Binary files a/examples/open-streets-dc/layers/dc_osm_highway/dc_osm_highway.shx and /dev/null differ
diff --git a/examples/open-streets-dc/layers/dc_osm_location/dc_osm_location.dbf b/examples/open-streets-dc/layers/dc_osm_location/dc_osm_location.dbf
deleted file mode 100644
index 01073e5..0000000
Binary files a/examples/open-streets-dc/layers/dc_osm_location/dc_osm_location.dbf and /dev/null differ
diff --git a/examples/open-streets-dc/layers/dc_osm_location/dc_osm_location.index b/examples/open-streets-dc/layers/dc_osm_location/dc_osm_location.index
deleted file mode 100644
index abd3bac..0000000
Binary files a/examples/open-streets-dc/layers/dc_osm_location/dc_osm_location.index and /dev/null differ
diff --git a/examples/open-streets-dc/layers/dc_osm_location/dc_osm_location.prj b/examples/open-streets-dc/layers/dc_osm_location/dc_osm_location.prj
deleted file mode 100644
index 379ef7c..0000000
--- a/examples/open-streets-dc/layers/dc_osm_location/dc_osm_location.prj
+++ /dev/null
@@ -1 +0,0 @@
-GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],TOWGS84[0,0,0,0,0,0,0],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]
\ No newline at end of file
diff --git a/examples/open-streets-dc/layers/dc_osm_location/dc_osm_location.shp b/examples/open-streets-dc/layers/dc_osm_location/dc_osm_location.shp
deleted file mode 100644
index 2aed4bc..0000000
Binary files a/examples/open-streets-dc/layers/dc_osm_location/dc_osm_location.shp and /dev/null differ
diff --git a/examples/open-streets-dc/layers/dc_osm_location/dc_osm_location.shx b/examples/open-streets-dc/layers/dc_osm_location/dc_osm_location.shx
deleted file mode 100644
index a9d9905..0000000
Binary files a/examples/open-streets-dc/layers/dc_osm_location/dc_osm_location.shx and /dev/null differ
diff --git a/examples/open-streets-dc/layers/dc_osm_natural/dc_osm_natural.dbf b/examples/open-streets-dc/layers/dc_osm_natural/dc_osm_natural.dbf
deleted file mode 100644
index ee14c8c..0000000
Binary files a/examples/open-streets-dc/layers/dc_osm_natural/dc_osm_natural.dbf and /dev/null differ
diff --git a/examples/open-streets-dc/layers/dc_osm_natural/dc_osm_natural.index b/examples/open-streets-dc/layers/dc_osm_natural/dc_osm_natural.index
deleted file mode 100644
index c03587b..0000000
Binary files a/examples/open-streets-dc/layers/dc_osm_natural/dc_osm_natural.index and /dev/null differ
diff --git a/examples/open-streets-dc/layers/dc_osm_natural/dc_osm_natural.prj b/examples/open-streets-dc/layers/dc_osm_natural/dc_osm_natural.prj
deleted file mode 100644
index 379ef7c..0000000
--- a/examples/open-streets-dc/layers/dc_osm_natural/dc_osm_natural.prj
+++ /dev/null
@@ -1 +0,0 @@
-GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],TOWGS84[0,0,0,0,0,0,0],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]
\ No newline at end of file
diff --git a/examples/open-streets-dc/layers/dc_osm_natural/dc_osm_natural.shp b/examples/open-streets-dc/layers/dc_osm_natural/dc_osm_natural.shp
deleted file mode 100644
index 9329c18..0000000
Binary files a/examples/open-streets-dc/layers/dc_osm_natural/dc_osm_natural.shp and /dev/null differ
diff --git a/examples/open-streets-dc/layers/dc_osm_natural/dc_osm_natural.shx b/examples/open-streets-dc/layers/dc_osm_natural/dc_osm_natural.shx
deleted file mode 100644
index a33d268..0000000
Binary files a/examples/open-streets-dc/layers/dc_osm_natural/dc_osm_natural.shx and /dev/null differ
diff --git a/examples/open-streets-dc/layers/osm-landusages.dbf b/examples/open-streets-dc/layers/osm-landusages.dbf
new file mode 100644
index 0000000..b9102e8
Binary files /dev/null and b/examples/open-streets-dc/layers/osm-landusages.dbf differ
diff --git a/examples/open-streets-dc/layers/osm-landusages.prj b/examples/open-streets-dc/layers/osm-landusages.prj
new file mode 100644
index 0000000..fc5d912
--- /dev/null
+++ b/examples/open-streets-dc/layers/osm-landusages.prj
@@ -0,0 +1 @@
+PROJCS["Popular_Visualisation_CRS_Mercator_deprecated",GEOGCS["GCS_Popular Visualisation CRS",DATUM["D_Popular_Visualisation_Datum",SPHEROID["Popular_Visualisation_Sphere",6378137,0]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Mercator"],PARAMETER["central_meridian",0],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["Meter",1],PARAMETER["standard_parallel_1",0.0]]
\ No newline at end of file
diff --git a/examples/open-streets-dc/layers/osm-landusages.shp b/examples/open-streets-dc/layers/osm-landusages.shp
new file mode 100644
index 0000000..0039f9a
Binary files /dev/null and b/examples/open-streets-dc/layers/osm-landusages.shp differ
diff --git a/examples/open-streets-dc/layers/osm-landusages.shx b/examples/open-streets-dc/layers/osm-landusages.shx
new file mode 100644
index 0000000..e787acd
Binary files /dev/null and b/examples/open-streets-dc/layers/osm-landusages.shx differ
diff --git a/examples/open-streets-dc/layers/osm-mainroads.dbf b/examples/open-streets-dc/layers/osm-mainroads.dbf
new file mode 100644
index 0000000..7c0f439
Binary files /dev/null and b/examples/open-streets-dc/layers/osm-mainroads.dbf differ
diff --git a/examples/open-streets-dc/layers/osm-mainroads.prj b/examples/open-streets-dc/layers/osm-mainroads.prj
new file mode 100644
index 0000000..fc5d912
--- /dev/null
+++ b/examples/open-streets-dc/layers/osm-mainroads.prj
@@ -0,0 +1 @@
+PROJCS["Popular_Visualisation_CRS_Mercator_deprecated",GEOGCS["GCS_Popular Visualisation CRS",DATUM["D_Popular_Visualisation_Datum",SPHEROID["Popular_Visualisation_Sphere",6378137,0]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Mercator"],PARAMETER["central_meridian",0],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["Meter",1],PARAMETER["standard_parallel_1",0.0]]
\ No newline at end of file
diff --git a/examples/open-streets-dc/layers/osm-mainroads.shp b/examples/open-streets-dc/layers/osm-mainroads.shp
new file mode 100644
index 0000000..1e0298e
Binary files /dev/null and b/examples/open-streets-dc/layers/osm-mainroads.shp differ
diff --git a/examples/open-streets-dc/layers/osm-mainroads.shx b/examples/open-streets-dc/layers/osm-mainroads.shx
new file mode 100644
index 0000000..6713446
Binary files /dev/null and b/examples/open-streets-dc/layers/osm-mainroads.shx differ
diff --git a/examples/open-streets-dc/layers/osm-motorways.dbf b/examples/open-streets-dc/layers/osm-motorways.dbf
new file mode 100644
index 0000000..489d6c9
Binary files /dev/null and b/examples/open-streets-dc/layers/osm-motorways.dbf differ
diff --git a/examples/open-streets-dc/layers/osm-motorways.prj b/examples/open-streets-dc/layers/osm-motorways.prj
new file mode 100644
index 0000000..fc5d912
--- /dev/null
+++ b/examples/open-streets-dc/layers/osm-motorways.prj
@@ -0,0 +1 @@
+PROJCS["Popular_Visualisation_CRS_Mercator_deprecated",GEOGCS["GCS_Popular Visualisation CRS",DATUM["D_Popular_Visualisation_Datum",SPHEROID["Popular_Visualisation_Sphere",6378137,0]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Mercator"],PARAMETER["central_meridian",0],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["Meter",1],PARAMETER["standard_parallel_1",0.0]]
\ No newline at end of file
diff --git a/examples/open-streets-dc/layers/osm-motorways.shp b/examples/open-streets-dc/layers/osm-motorways.shp
new file mode 100644
index 0000000..9612015
Binary files /dev/null and b/examples/open-streets-dc/layers/osm-motorways.shp differ
diff --git a/examples/open-streets-dc/layers/osm-motorways.shx b/examples/open-streets-dc/layers/osm-motorways.shx
new file mode 100644
index 0000000..7da4431
Binary files /dev/null and b/examples/open-streets-dc/layers/osm-motorways.shx differ
diff --git a/examples/open-streets-dc/layers/osm-places.dbf b/examples/open-streets-dc/layers/osm-places.dbf
new file mode 100644
index 0000000..7ed680c
Binary files /dev/null and b/examples/open-streets-dc/layers/osm-places.dbf differ
diff --git a/examples/open-streets-dc/layers/osm-places.prj b/examples/open-streets-dc/layers/osm-places.prj
new file mode 100644
index 0000000..fc5d912
--- /dev/null
+++ b/examples/open-streets-dc/layers/osm-places.prj
@@ -0,0 +1 @@
+PROJCS["Popular_Visualisation_CRS_Mercator_deprecated",GEOGCS["GCS_Popular Visualisation CRS",DATUM["D_Popular_Visualisation_Datum",SPHEROID["Popular_Visualisation_Sphere",6378137,0]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Mercator"],PARAMETER["central_meridian",0],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["Meter",1],PARAMETER["standard_parallel_1",0.0]]
\ No newline at end of file
diff --git a/examples/open-streets-dc/layers/osm-places.shp b/examples/open-streets-dc/layers/osm-places.shp
new file mode 100644
index 0000000..2807c8f
Binary files /dev/null and b/examples/open-streets-dc/layers/osm-places.shp differ
diff --git a/examples/open-streets-dc/layers/osm-places.shx b/examples/open-streets-dc/layers/osm-places.shx
new file mode 100644
index 0000000..fa47b4f
Binary files /dev/null and b/examples/open-streets-dc/layers/osm-places.shx differ
diff --git a/examples/open-streets-dc/layers/osm-roads.dbf b/examples/open-streets-dc/layers/osm-roads.dbf
new file mode 100644
index 0000000..39d57ba
Binary files /dev/null and b/examples/open-streets-dc/layers/osm-roads.dbf differ
diff --git a/examples/open-streets-dc/layers/osm-roads.prj b/examples/open-streets-dc/layers/osm-roads.prj
new file mode 100644
index 0000000..fc5d912
--- /dev/null
+++ b/examples/open-streets-dc/layers/osm-roads.prj
@@ -0,0 +1 @@
+PROJCS["Popular_Visualisation_CRS_Mercator_deprecated",GEOGCS["GCS_Popular Visualisation CRS",DATUM["D_Popular_Visualisation_Datum",SPHEROID["Popular_Visualisation_Sphere",6378137,0]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Mercator"],PARAMETER["central_meridian",0],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["Meter",1],PARAMETER["standard_parallel_1",0.0]]
\ No newline at end of file
diff --git a/examples/open-streets-dc/layers/osm-roads.shp b/examples/open-streets-dc/layers/osm-roads.shp
new file mode 100644
index 0000000..eb49cf0
Binary files /dev/null and b/examples/open-streets-dc/layers/osm-roads.shp differ
diff --git a/examples/open-streets-dc/layers/osm-roads.shx b/examples/open-streets-dc/layers/osm-roads.shx
new file mode 100644
index 0000000..951e2d7
Binary files /dev/null and b/examples/open-streets-dc/layers/osm-roads.shx differ
diff --git a/examples/open-streets-dc/layers/osm-waterareas.dbf b/examples/open-streets-dc/layers/osm-waterareas.dbf
new file mode 100644
index 0000000..092f3ff
Binary files /dev/null and b/examples/open-streets-dc/layers/osm-waterareas.dbf differ
diff --git a/examples/open-streets-dc/layers/osm-waterareas.prj b/examples/open-streets-dc/layers/osm-waterareas.prj
new file mode 100644
index 0000000..fc5d912
--- /dev/null
+++ b/examples/open-streets-dc/layers/osm-waterareas.prj
@@ -0,0 +1 @@
+PROJCS["Popular_Visualisation_CRS_Mercator_deprecated",GEOGCS["GCS_Popular Visualisation CRS",DATUM["D_Popular_Visualisation_Datum",SPHEROID["Popular_Visualisation_Sphere",6378137,0]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Mercator"],PARAMETER["central_meridian",0],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["Meter",1],PARAMETER["standard_parallel_1",0.0]]
\ No newline at end of file
diff --git a/examples/open-streets-dc/layers/osm-waterareas.shp b/examples/open-streets-dc/layers/osm-waterareas.shp
new file mode 100644
index 0000000..0fa28df
Binary files /dev/null and b/examples/open-streets-dc/layers/osm-waterareas.shp differ
diff --git a/examples/open-streets-dc/layers/osm-waterareas.shx b/examples/open-streets-dc/layers/osm-waterareas.shx
new file mode 100644
index 0000000..169c262
Binary files /dev/null and b/examples/open-streets-dc/layers/osm-waterareas.shx differ
diff --git a/examples/open-streets-dc/project.mml b/examples/open-streets-dc/project.mml
index d1fd12c..5f116b2 100644
--- a/examples/open-streets-dc/project.mml
+++ b/examples/open-streets-dc/project.mml
@@ -1,17 +1,19 @@
 {
   "bounds": [
-    -180,
-    -85.05112877980659,
-    180,
-    85.05112877980659
+    -77.1408,
+    38.779,
+    -76.893,
+    39.0088
   ],
   "center": [
-    -77.02945411205174,
-    38.89816813905991,
-    13
+    -77.036,
+    38.9013,
+    12
   ],
   "format": "png",
-  "interactivity": false,
+  "interactivity": {
+    "fields": []
+  },
   "minzoom": 7,
   "maxzoom": 18,
   "srs": "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over",
@@ -22,86 +24,208 @@
   ],
   "Layer": [
     {
-      "id": "dcgis-water",
-      "name": "dcgis-water",
+      "id": "landusages",
+      "name": "landusages",
+      "srs": "",
+      "class": "",
+      "Datasource": {
+        "file": "layers/osm-landusages.shp",
+        "id": "natural",
+        "srs": "",
+        "project": "open-streets-dc"
+      },
+      "geometry": "polygon",
+      "extent": [
+        -77.11930264157353,
+        38.80315049119033,
+        -76.90914770514456,
+        38.995960228962566
+      ],
+      "srs-name": "autodetect",
+      "advanced": {}
+    },
+    {
+      "geometry": "polygon",
+      "extent": [
+        -77.04786361160811,
+        38.791636589623394,
+        -77.0164310862004,
+        38.8781479
+      ],
+      "id": "ocean",
+      "class": "",
+      "Datasource": {
+        "file": "layers/dc-coastline.shp"
+      },
+      "srs-name": "autodetect",
+      "srs": "",
+      "advanced": {},
+      "name": "ocean"
+    },
+    {
+      "id": "water",
+      "name": "water",
       "srs": "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over",
-      "class": "water",
+      "class": "",
       "Datasource": {
-        "file": "layers/dcgis_water/WaterPly_900913.shp",
+        "file": "layers/osm-waterareas.shp",
         "id": "dcgis-water",
         "srs": "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over",
         "project": "open-streets-dc"
       },
-      "geometry": "polygon"
+      "geometry": "polygon",
+      "extent": [
+        -77.11979289660997,
+        38.853243259126195,
+        -76.93917631623486,
+        38.98706755172806
+      ],
+      "srs-name": "900913",
+      "advanced": {}
     },
     {
-      "id": "natural",
-      "name": "natural",
-      "srs": "",
-      "class": "natural",
+      "geometry": "linestring",
+      "extent": [
+        -77.11718944069726,
+        38.792397209478516,
+        -76.90951835729004,
+        38.995661712388085
+      ],
+      "id": "tunnels",
+      "class": "",
       "Datasource": {
-        "file": "layers/dc_osm_natural/dc_osm_natural.shp",
-        "id": "natural",
-        "srs": "",
-        "project": "open-streets-dc"
+        "file": "layers/osm-roads.shp",
+        "cache-features": "on"
       },
-      "geometry": "polygon"
+      "srs-name": "autodetect",
+      "srs": "",
+      "advanced": {
+        "cache-features": "on"
+      },
+      "name": "tunnels"
     },
     {
-      "id": "highway-outline",
-      "name": "highway-outline",
+      "geometry": "linestring",
+      "extent": [
+        -77.11718944069726,
+        38.792397209478516,
+        -76.90951835729004,
+        38.995661712388085
+      ],
+      "id": "roads",
+      "class": "",
+      "Datasource": {
+        "file": "layers/osm-roads.shp"
+      },
+      "srs-name": "autodetect",
       "srs": "",
-      "class": "highway line",
+      "advanced": {},
+      "name": "roads"
+    },
+    {
+      "geometry": "linestring",
+      "extent": [
+        -77.11652930790603,
+        38.820831361563556,
+        -76.91065941957852,
+        38.99246390729336
+      ],
+      "id": "mainroads",
+      "class": "",
       "Datasource": {
-        "file": "layers/dc_osm_highway/dc_osm_highway.shp",
-        "id": "highway-outline",
-        "srs": "",
-        "project": "open-streets-dc"
+        "file": "layers/osm-mainroads.shp"
       },
-      "geometry": "linestring"
+      "srs-name": "autodetect",
+      "srs": "",
+      "advanced": {},
+      "name": "mainroads"
     },
     {
-      "id": "highway",
-      "name": "highway",
+      "geometry": "linestring",
+      "extent": [
+        -77.11663688162237,
+        38.792397209478516,
+        -76.91316118128722,
+        38.94633988420798
+      ],
+      "id": "motorways",
+      "class": "",
+      "Datasource": {
+        "file": "layers/osm-motorways.shp"
+      },
+      "srs-name": "autodetect",
       "srs": "",
-      "class": "highway fill",
+      "advanced": {},
+      "name": "motorways"
+    },
+    {
+      "geometry": "linestring",
+      "extent": [
+        -77.11718944069726,
+        38.792397209478516,
+        -76.90951835729004,
+        38.995661712388085
+      ],
+      "id": "bridges",
+      "class": "",
       "Datasource": {
-        "file": "layers/dc_osm_highway/dc_osm_highway.shp",
-        "id": "highway",
-        "srs": "",
-        "project": "open-streets-dc"
+        "file": "layers/osm-roads.shp",
+        "id": "bridges",
+        "project": "open-streets-dc",
+        "srs": ""
       },
-      "geometry": "linestring"
+      "srs-name": "autodetect",
+      "srs": "",
+      "advanced": {},
+      "name": "bridges"
     },
     {
-      "id": "location",
-      "name": "location",
+      "id": "places",
+      "name": "places",
       "srs": "",
-      "class": "location",
+      "class": "",
       "Datasource": {
-        "file": "layers/dc_osm_location/dc_osm_location.shp",
+        "file": "layers/osm-places.shp",
         "id": "location",
         "srs": "",
         "project": "open-streets-dc"
       },
-      "geometry": "point"
+      "geometry": "point",
+      "extent": [
+        -77.10775630000002,
+        38.8265324,
+        -76.91636240000008,
+        38.992610099999965
+      ],
+      "srs-name": "autodetect",
+      "advanced": {}
     },
     {
-      "id": "highway-label",
-      "name": "highway-label",
+      "id": "road-label",
+      "name": "road-label",
       "srs": "",
-      "class": "highway-label",
+      "class": "",
       "Datasource": {
-        "file": "layers/dc_osm_highway/dc_osm_highway.shp",
+        "file": "layers/osm-roads.shp",
         "id": "highway-label",
         "srs": "",
         "project": "open-streets-dc"
       },
-      "geometry": "linestring"
+      "geometry": "linestring",
+      "extent": [
+        -77.11718944069726,
+        38.792397209478516,
+        -76.90951835729004,
+        38.995661712388085
+      ],
+      "srs-name": "autodetect",
+      "advanced": {}
     }
   ],
-  "legend": "<!-- This legend uses Unicode box-drawing characters to approxmate line styles. -->\n<span style='color:#DD3A85'>━</span> Motorways <br />\n<span style='color:#FFC81A'>━</span> Main roads <br />\n<span style='color:#fff'>━</span> Other roads <br />\n<span style='color:#69B'>┉</span> Bike paths <br />\n<span style='color:#6B9'>┉</span> Foot paths <br />\n<span style='color:#cea'>▉</span> Forest <br />\n<span style='color:#c0d8ff'>▉</span> Water ",
+  "scale": 1,
+  "metatile": 2,
+  "legend": "<!-- This legend uses Unicode box-drawing characters to approxmate line styles. -->\n<span style='color:#F56544'>━</span> Motorways <br />\n<span style='color:#FFC53C'>━</span> Main roads <br />\n<span style='color:#ccc'>━</span> Other roads <br />\n<span style='color:#AC9'>┉</span> Bike paths <br />\n<span style='color:#9CA'>┉</span> Foot paths <br />\n<span style='color:#cea'>▉</span> Park <br />\n<span style='color:#f8e8c8'>▉</span> School <br />\n<span style='color:#c0d8 [...]
   "name": "Open Streets, DC",
   "description": "An example of street-level map design.",
   "attribution": "Data used by this map is © OpenStreetMap contributors,  CC-BY-SA. See <http://openstreetmap.org> for more info."
-}
\ No newline at end of file
+}
diff --git a/examples/open-streets-dc/style.mss b/examples/open-streets-dc/style.mss
index af0ce18..1f39d04 100644
--- a/examples/open-streets-dc/style.mss
+++ b/examples/open-streets-dc/style.mss
@@ -5,48 +5,62 @@ Open Streets, DC
 
 *An example of street-level map design.*
 
-Data used by this map is © OpenStreetMap contributors, 
-CC-BY-SA. See <http://openstreetmap.org> for more info.
+Data used by this map is © OpenStreetMap contributors and
+distributed under the terms of the Open Database License.
+See <http://www.openstreetmap.org/copyright> for details.
 
-This map makes use of OpenStreetMap shapefile extracts
-provided by CloudMade at <http://downloads.cloudmade.com>.
-You can swap out the DC data with any other shapefiles 
-provided by CloudMade to get a map of your area.
+Pattern images derived from designs by Subtle Patterns and 
+licensed CC-BY-SA. See <http://subtlepatterns.com> for details.
 
-To prepare a CloudMade shapefiles zip package for TileMill,
-download it and run the following commands:
-
-    unzip your_area.shapefiles.zip
-    cd your_area.shapefiles
-    shapeindex *.shp
-    for i in *.shp; do \
-        zip `basename $i .shp` `basename $i shp`*; done
+The shapefiles used in this project are based on those
+provided by Mike Migurski at <http://metro.teczno.com>.
+You can swap out the DC data for any other city there by
+downloading the Imposm shapefile package.
 
 ***********************************************************/
 
 /* ---- PALETTE ---- */
 
 @water:#c0d8ff;
- at forest:#cea;
- at land:#fff;
+ at park:#cea;
+ at land:#f5fdf0;
+ at school:#f8e8c8;
 
 Map {
   background-color:@land;
 }
 
-.natural[TYPE='water'],
-.water {
+#water,
+#ocean {
   polygon-fill:@water;
+  polygon-gamma:0.5; // reduces gaps between shapes
+  polygon-pattern-file:url(images/water.png);
+  polygon-pattern-comp-op:color-burn;
+  polygon-pattern-alignment:global; // keeps it seamless
 }
 
-.natural[TYPE='forest'] {
-  polygon-fill:@forest;
+#landusages[zoom>6] {
+  [type='forest'],
+  [type='wood'] {
+    polygon-fill:@park;
+    polygon-pattern-file:url(images/wood.png);
+    polygon-pattern-comp-op:multiply;
+  }
+  [type='cemetery'],
+  [type='common'],
+  [type='golf_course'],
+  [type='park'],
+  [type='pitch'],
+  [type='recreation_ground'],
+  [type='village_green'] {
+    polygon-fill:@park;
+  }
 }
 
-/* These are not used, but if customizing this style you may
-wish to use OSM's land shapefiles. See the wiki for info:
-<http://wiki.openstreetmap.org/wiki/Mapnik#World_boundaries> */
-#shoreline_300[zoom<11],
-#processed_p[zoom>=11] {
-  polygon-fill: @land;
+#landusages[zoom>=12] {
+  [type='school'],
+  [type='college'],
+  [type='university'] {
+    polygon-fill: @school;
+  }
 }
diff --git a/examples/road-trip/.thumb.png b/examples/road-trip/.thumb.png
index a3450b0..8bc9d31 100644
Binary files a/examples/road-trip/.thumb.png and b/examples/road-trip/.thumb.png differ
diff --git a/examples/road-trip/project.mml b/examples/road-trip/project.mml
index 8323fb7..9e49862 100644
--- a/examples/road-trip/project.mml
+++ b/examples/road-trip/project.mml
@@ -155,8 +155,10 @@
       }
     }
   ],
+  "scale": 1,
+  "metatile": 2,
   "name": "Road Trip",
   "description": "A map of the United States inspired by the impossible-to-fold maps in your glovebox.",
   "attribution": "",
   "legend": ""
-}
\ No newline at end of file
+}
diff --git a/index.js b/index.js
index e540f2d..1d058bb 100755
--- a/index.js
+++ b/index.js
@@ -1,6 +1,9 @@
 #!/usr/bin/env node
 
 var fs = require('fs');
+var path = require('path');
+// node v6 -> v8 compatibility
+var existsSync = require('fs').existsSync || require('path').existsSync;
 
 process.title = 'tilemill';
 
@@ -10,30 +13,16 @@ process.title = 'tilemill';
 process.argv[0] = 'node';
 
 if (process.platform === 'win32') {
-
     // HOME is undefined on windows
-    process.env.HOME = process.env.HOMEPATH;
-
-    // don't attempt symlink support at all -- just copy.
-    // @TODO write a dotfile next to the copy with the link
-    // "metadata" so we can monkeypatch readlink as well.
-    var cprSync = require('./lib/fsutil').cprSync;
-    fs.symlink = function(from,to,cb) {
-        try {
-            cprSync(from, to);
-            return cb();
-        } catch (err) {
-            return cb(err);
-        }
-    }
+    process.env.HOME = process.env.USERPROFILE;
+    process.env.PATH = "node_modules/mapnik/lib/mapnik/lib;node_modules/zipfile/lib;"+process.env.PATH;
 }
 
 // Default --config flag to user's home .tilemill.json config file.
 // @TODO find a more elegant way to set a default for this value
 // upstream in bones.
-var path = require('path');
 var config = path.join(process.env.HOME, '.tilemill/config.json');
-if (path.existsSync(config)) {
+if (existsSync(config)) {
     var argv = require('optimist').argv;
     argv.config = argv.config || config;
 }
diff --git a/lib/config.defaults.json b/lib/config.defaults.json
index b98092c..a8ebf55 100644
--- a/lib/config.defaults.json
+++ b/lib/config.defaults.json
@@ -1,13 +1,13 @@
 {
     "port": 20009,
     "tilePort": 20008,
-    "bufferSize": 128,
     "files": "~/Documents/MapBox",
     "examples": true,
     "sampledata": true,
     "host": false,
     "listenHost": "127.0.0.1",
+    "syncAPI": "http://api.tiles.mapbox.com",
     "syncURL": "https://tiles.mapbox.com",
-    "hostname": "localhost",
-    "server": false
+    "server": false,
+    "verbose": "on"
 }
diff --git a/lib/crashutil.js b/lib/crashutil.js
new file mode 100644
index 0000000..dc4df9f
--- /dev/null
+++ b/lib/crashutil.js
@@ -0,0 +1,51 @@
+var path = require('path');
+var child = require('child_process');
+var glob = require('glob');
+var chrono = require('chrono');
+
+var path_sep = process.platform === 'win32' ? '\\' : '/';
+
+function display_crash_log(callback) {
+    try {
+        if (process.platform == 'darwin') {
+            var now = new Date();
+            var crash_path = path.join(process.env.HOME, 'Library/Logs/DiagnosticReports');
+            var search = crash_path + '/node_' + now.format('Y-m-d') + '-*.crash';
+            var files = glob.sync(search);
+            if (files.length > 0) {
+                console.warn('[tilemill] found ' + files.length + ' crash logs for node');
+                // grab the latest
+                var latest = files[files.length - 1];
+                return callback(null, latest);
+            } else {
+                console.log('[tilemill] no crash logs found');
+                return callback(null, null);
+            }
+        } else if (process.platform === 'win32') {
+            var crash_path = path.join(process.env.HOMEPATH, 'AppData/Local/Microsoft/Windows/WER/ReportArchive/');
+            // normalize to unix paths to that glob works
+            //crash_path = crash_path.replace(/\\/g, '/');
+            // use cwd to workaround abs path bug: https://github.com/isaacs/node-glob/issues/40
+            var options = {cwd: crash_path};
+            var search = 'AppCrash_node.exe_*';
+            var files = glob.sync(search,options);
+            if (files.length > 0) {
+                console.warn('[tilemill] found ' + files.length + ' crash logs for node');
+                // grab the latest
+                var latest = path.join(crash_path, files[files.length - 1]);
+                return callback(null, latest);
+            } else {
+                console.log('[tilemill] no crash logs found');
+                return callback(null, null);
+            }
+        } else {
+            return callback(null, null);
+        }
+    } catch (err) {
+        return callback(err);
+    }
+}
+
+module.exports = {
+    display_crash_log: display_crash_log
+};
diff --git a/lib/fsutil.js b/lib/fsutil.js
index 1a2c630..88525f0 100644
--- a/lib/fsutil.js
+++ b/lib/fsutil.js
@@ -5,6 +5,8 @@ var path = require('path');
 var constants = require('constants');
 var _ = require('underscore');
 var mkdirp = require('mkdirp');
+// node v6 -> v8 compatibility
+var existsSync = require('fs').existsSync || require('path').existsSync;
 
 var path_sep = process.platform === 'win32' ? '\\' : '/';
 
@@ -53,7 +55,7 @@ function mkdirpSync(p, mode) {
     var created = [];
     while (ps.length) {
         created.push(ps.shift());
-        if (created.length > 1 && !path.existsSync(created.join(path_sep))) {
+        if (created.length > 1 && !existsSync(created.join(path_sep))) {
             var err = fs.mkdirSync(created.join(path_sep), 0755);
             if (err) return err;
         }
@@ -103,12 +105,25 @@ function cprSync(from, to) {
     }
 };
 
+// poor man's windows drive detection because
+// shelling out to `fsutil fsinfo drives`
+// would require admin and there does not appear to be an
+// ffi solution given https://github.com/Benvie/node-Windows/blob/ffi-registry/lib/driveAlias.js
+// uses fsutil
+function winDrives() {
+    var letters = 'abcdefghijklmnopqrstuvwxyz'.split('').filter(function(s){
+	    return existsSync(s+':\\');
+	});
+	return letters.map(function(s) { return s+':\\'});
+}
+
 module.exports = {
     read: read,
     readdir: readdir,
     mkdirp: mkdirp,
     mkdirpSync: mkdirpSync,
     cprSync: cprSync,
-    rm: rm
+    rm: rm,
+    winDrives: winDrives
 };
 
diff --git a/lib/gitutil.js b/lib/gitutil.js
new file mode 100755
index 0000000..e48a863
--- /dev/null
+++ b/lib/gitutil.js
@@ -0,0 +1,13 @@
+var exec = require('child_process').exec;
+var fs = require('fs');
+
+var child = exec('git describe --tags',
+  function(error, stdout, stderr) {
+    if (error !== null) {
+      console.log('exec error: ' + error);
+    } else {
+        var hash = stdout;
+        var version_file = hash + hash.slice(1, -10).replace('-', '.') + '\n';
+        fs.writeFileSync('VERSION', version_file);
+    }
+});
diff --git a/lib/redirect.js b/lib/redirect.js
new file mode 100644
index 0000000..0c33142
--- /dev/null
+++ b/lib/redirect.js
@@ -0,0 +1,18 @@
+function format(output) {
+    var prefix = '[' + process.title + '] ';
+    if (output[output.length - 1] === '\n') {
+        output = output.substring(0, output.length - 1);
+    }
+    console.warn(prefix + output.split('\n').join('\n' + prefix));
+}
+
+function onData(proc) {
+    proc.stdout.setEncoding('utf8');
+    proc.stdout.on('data', format.bind(global));
+    proc.stderr.setEncoding('utf8');
+    proc.stderr.on('data', format.bind(global));
+}
+
+module.exports = {
+    onData: onData
+};
diff --git a/lib/s3.js b/lib/s3.js
index 35c106e..e21e7c3 100644
--- a/lib/s3.js
+++ b/lib/s3.js
@@ -46,9 +46,6 @@ function list(options, callback) {
         options.prefix += '/';
     }
 
-    options.client = options.client ||
-        http.createClient(80, options.bucket + '.s3.amazonaws.com');
-
     // Limit the number of items returned by S3 to 100. Query multiple times
     // in order to retrieve the full set.
     var params = {
@@ -58,10 +55,27 @@ function list(options, callback) {
     options.prefix && (params.prefix = options.prefix);
     options.marker && (params.marker = options.marker);
 
-    var req = options.client.request(
-        'GET', '/?' + querystring.stringify(params),
-        {host: options.client.host}
-    );
+    var proxy = options.proxy ? url.parse(options.proxy) : undefined,
+        host = options.bucket + '.s3.amazonaws.com',
+        query = '/?' + querystring.stringify(params);
+
+    var requestOpts = proxy ? {
+        hostname: proxy.hostname,
+        port: proxy.port,
+        path: 'http://' + host + query,
+        headers: {
+            Host: host
+        }
+    } : {
+        hostname: host,
+        path: query
+    };
+    if (proxy && proxy.auth) {
+        requestOpts.headers['proxy-authorization'] =
+            'Basic ' + new Buffer(proxy.auth).toString('base64');
+    }
+
+    var req = http.get(requestOpts);
     req.on('response', function(res) {
         var xml = '';
         res.setEncoding('utf8');
diff --git a/lib/ubuntu_gui_workaround.js b/lib/ubuntu_gui_workaround.js
new file mode 100644
index 0000000..ac5fe77
--- /dev/null
+++ b/lib/ubuntu_gui_workaround.js
@@ -0,0 +1,74 @@
+var path = require('path');
+var child_process = require('child_process');
+
+function check(callback) {
+    if (process.platform == 'linux') {
+        // lsb_release -rs
+        child_process.exec('lsb_release -rs',
+            function (error, stdout, stderr) {
+                if (stdout && stdout.search('.') > -1) {
+                    var major = parseInt(stdout.split('.')[0]);
+                    var minor = parseInt(stdout.split('.')[1]);
+                    // natty or before
+                    if (major <= 10) {
+                        return callback(true);
+                    } else if (major === 11 && minor <= 4) {
+                        return callback(true);
+                    } else {
+                        return callback(false);
+                    }
+                } else {
+                    return callback(false);
+                }
+            });
+    } else {
+        return callback(false);
+    }
+}
+
+function get_client(options) {
+    //return require('topcube')(options);
+    options.u = options.url
+    options.n = options.name;
+    options.W = options.width;
+    options.H = options.height;
+    options.w = options.minwidth;
+    options.h = options.minheight;
+
+    var client;
+    var keys = [];
+    switch (process.platform) {
+    case 'linux':
+        client = path.resolve(__dirname + (/0\.4\./.test(process.version) ? '/../node_modules/topcube/build/default/topcube' : '/../node_modules/topcube/build/Release/topcube'));
+        //keys = ['url', 'name', 'width', 'height', 'minwidth', 'minheight'];
+        keys = ['u', 'n', 'W', 'H', 'w', 'h'];
+        break;
+    default:
+        console.warn('invalid platform for custom topcube client: ' + process.platform);
+        return null;
+        break;
+    }
+
+    var args = [];
+    for (var key in options) {
+        if (keys.indexOf(key) !== -1) {
+            if (key.length == 1) {
+                args.push('-' + key);
+                args.push(options[key]);
+            }
+        }
+    }
+    var child = child_process.spawn(client, args);
+    child.stdout.pipe(process.stdout);
+    child.stderr.pipe(process.stderr);
+    child.on('exit', function(code) {
+        console.log('exiting custom topcube client');
+        process.exit(code);
+    });
+    return child;
+};
+
+module.exports = {
+    check: check,
+    get_client: get_client
+};
diff --git a/models/Config.bones b/models/Config.bones
index b75daf6..ed1b847 100644
--- a/models/Config.bones
+++ b/models/Config.bones
@@ -9,11 +9,15 @@ model.prototype.schema = {
         'port': {
             'type': 'integer'
         },
-        'bufferSize': {
-            'type': 'integer'
-        },
         'files': {
             'type': 'string'
+        },
+        'httpProxy': {
+            'type': 'string'
+        },
+        'verbose': {
+            'type': 'string',
+            'enum': ['on', 'off'],
         }
     }
 };
diff --git a/models/Config.server.bones b/models/Config.server.bones
index 9f06345..df360d2 100644
--- a/models/Config.server.bones
+++ b/models/Config.server.bones
@@ -27,7 +27,6 @@ models.Config.prototype.sync = function(method, model, success, error) {
     case 'update':
         // Filter out keys that may not be written.
         var allowedKeys = [
-            'bufferSize',
             'files',
             'syncAccount',
             'syncAccessToken',
@@ -35,10 +34,12 @@ models.Config.prototype.sync = function(method, model, success, error) {
             'updatesTime',
             'updatesVersion',
             'profile',
-            'guid'
+            'guid',
+            'httpProxy',
+            'verbose'
         ];
         var data = _(model.toJSON()).reduce(function(memo, val, key) {
-            if (key === 'files') val = val.replace('~', process.env.HOME);
+            if (key === 'files') val = val.replace(/^~/, process.env.HOME);
             if (_(allowedKeys).include(key)) memo[key] = val;
             return memo;
         }, {});
@@ -52,7 +53,6 @@ models.Config.prototype.sync = function(method, model, success, error) {
             // Catch & blow away invalid user JSON.
             try { current = JSON.parse(current); }
             catch (e) { current = {}; }
-
             data = _(current).extend(data);
             fs.writeFile(paths.user, JSON.stringify(data, null, 2), this);
         // May contain sensitive info. Set secure permissions.
@@ -72,3 +72,9 @@ models.Config.prototype.sync = function(method, model, success, error) {
     }
 };
 
+models.Config.prototype.validate = function(attr) {
+    if (attr.httpProxy && attr.httpProxy.indexOf('http://') !== 0) {
+        return "HTTP Proxy must start with http://";
+    }
+    return this.validateAttributes(attr);
+}
diff --git a/models/Datasource.server.bones b/models/Datasource.server.bones
index c6b9517..e23e10b 100644
--- a/models/Datasource.server.bones
+++ b/models/Datasource.server.bones
@@ -14,6 +14,9 @@ models.Datasource.prototype.sync = function(method, model, success, error) {
     if (!options) return error(new Error('options are required.'));
     if (!options.id) return error(new Error('id is required.'));
     if (!options.project) return error(new Error('project is required.'));
+    if (options.file) {
+        options.file = options.file.trim().replace(/^~/, process.env.HOME);
+    }
 
     millstone.resolve({
         mml: {
@@ -27,20 +30,70 @@ models.Datasource.prototype.sync = function(method, model, success, error) {
         base: path.join(config.files, 'project', options.project),
         cache: path.join(config.files, 'cache')
     }, function(err, mml) {
-        if (err) return error(err);
+        if (err) {
+            if (process.env.NODE_ENV === 'development') console.log('[tilemill] problem loading datasource ' + err.stack || err.message);
+            return error(err);
+        }
+
+        // "Sticky" options are those that should be passed to the layer model
+        // when it saves the datasource in the mml that is later used for rendering
+        // NOTE: 'row_limit' is not a sticky option intentially - it needs to get thrown away because we only
+        // want to limit datasource queries for attribute data and not for rendering
+        var sticky_options = {};
 
         try {
             mml.Layer[0].Datasource = _(mml.Layer[0].Datasource).defaults(options);
 
-            // Some mapnik datasource accept 'row_limit` (like postgis, shape)
+            // Some mapnik datasources accept 'row_limit` (like postgis, shape)
             // those that do not will be restricted during the featureset loop below
             var row_limit = 500;
-            //mml.Layer[0].Datasource = _(mml.Layer[0].Datasource).defaults({row_limit:row_limit});
-            //console.log(mml.Layer[0].Datasource);
-            var source = new mapnik.Datasource(mml.Layer[0].Datasource);
+            mml.Layer[0].Datasource = _(mml.Layer[0].Datasource).defaults({
+                row_limit: row_limit
+                });
+
+            // simplistic validation that subselects have the key_field string present
+            // not a proper parser, but this is not the right place to be parsing SQL
+            // https://github.com/mapbox/tilemill/issues/1509
+            if (mml.Layer[0].Datasource.table !== undefined
+                && mml.Layer[0].Datasource.key_field !== undefined
+                && mml.Layer[0].Datasource.table.match(/select /i)
+                && mml.Layer[0].Datasource.table.indexOf('*') == -1
+                && mml.Layer[0].Datasource.table.search(mml.Layer[0].Datasource.key_field) == -1) {
+                    return error(new Error("Your SQL subquery needs to explicitly include the custom key_field: '" + mml.Layer[0].Datasource.key_field + "' or use 'select *' to request all fields"));
+            }
+
+            var source;
+
+            // https://github.com/mapbox/tilemill/issues/1754
+            if (mml.Layer[0].Datasource.type == 'ogr') {
+                try {
+                    source = new mapnik.Datasource(mml.Layer[0].Datasource);
+                } catch (err) {
+                    var rethrow = true;
+                    if (!mml.Layer[0].Datasource.layer_by_index
+                        && !mml.Layer[0].Datasource.layer
+                        && err.message
+                        && err.message.indexOf('OGR Plugin: missing <layer>') != -1) {
+                        var layers = err.message.split("are: ")[1];
+                        var layer_names = _(layers.trim().split(/[\s,]+/)).compact();
+                        if (layer_names.length > 1) {
+                            var better_error = new Error("This datasource has multiple layers:\n" + layer_names + '\n(pass layer=<name> to the Advanced input to pick one)');
+                            throw better_error;
+                        } else {
+                            rethrow = false;
+                            sticky_options.layer_by_index = 0;
+                            mml.Layer[0].Datasource.layer_by_index = 0;
+                            source = new mapnik.Datasource(mml.Layer[0].Datasource);
+                        }
+                    }
+                    if (rethrow) throw err;
+                }
+            } else {
+                source = new mapnik.Datasource(mml.Layer[0].Datasource);
+            }
 
             var features = [];
-            if (options.features || options.info) {
+            if (!(source.type == "raster") && (options.features || options.info)) {
                 var featureset = source.featureset();
                 for (var i = 0, feat;
                     i < row_limit && (feat = featureset.next(true));
@@ -49,6 +102,21 @@ models.Datasource.prototype.sync = function(method, model, success, error) {
                 }
             }
 
+            // Convert datasource extent to lon/lat when saving
+            var layerProj = new mapnik.Projection(mml.Layer[0].srs),
+                unProj = new mapnik.Projection('+proj=longlat +ellps=WGS84 +no_defs'),
+                trans = new mapnik.ProjTransform(layerProj, unProj),
+                unproj_extent = trans.forward(source.extent());
+            // clamp to valid unproj_extents
+            (unproj_extent[0] < -180) && (unproj_extent[0] = -180);
+            (unproj_extent[1] < -85.051) && (unproj_extent[1] = -85.051);
+            (unproj_extent[2] > 180) && (unproj_extent[2] = 180);
+            (unproj_extent[3] > 85.051) && (unproj_extent[3] = 85.051);
+
+            if (unproj_extent[2] < unproj_extent[0] || unproj_extent[3] < unproj_extent[1]) {
+                throw new Error("Detected out of bounds geographic extent (" + unproj_extent + ") for layer '" + options.id + "'. Please ensure that the SRS for this layer is correct. Its native extent is '" + source.extent() + "'");
+            }
+
             var desc = source.describe();
             var datasource = {
                 id: options.id,
@@ -57,9 +125,13 @@ models.Datasource.prototype.sync = function(method, model, success, error) {
                 fields: desc.fields,
                 features: options.features ? features : [],
                 type: desc.type,
-                geometry_type: desc.type === 'raster' ? 'raster' : desc.geometry_type
+                geometry_type: desc.type === 'raster' ? 'raster' : desc.geometry_type,
+                unproj_extent: unproj_extent,
+                sticky_options:sticky_options,
+                extent: source.extent().join(',')
             };
 
+
             // Process fields and calculate min/max values.
             for (var f in datasource.fields) {
                 var values = _(features).pluck(f);
@@ -67,7 +139,11 @@ models.Datasource.prototype.sync = function(method, model, success, error) {
                 datasource.fields[f] = { type: type };
                 if (options.features || options.info) {
                     datasource.fields[f].max = type === 'String'
-                        ? _(values).max(function(v) { return (v||'').length })
+                        ? (function() {
+                            var val = _(values).max(function(v) { return (v||'').length });
+                            return (val && val.length > 55) ?
+                              val.slice(0, 55 - 3) + '...' : val;
+                        })()
                         : _(values).max();
                     datasource.fields[f].min = type === 'String'
                         ? _(values).min(function(v) { return (v||'').length })
diff --git a/models/Export.bones b/models/Export.bones
index 563b635..05f4937 100644
--- a/models/Export.bones
+++ b/models/Export.bones
@@ -37,6 +37,10 @@ model.prototype.schema = {
             'required': true,
             'enum': ['png', 'pdf', 'svg', 'mbtiles', 'upload', 'sync']
         },
+        'metatile': {
+            'type': 'integer',
+            'required': true
+        },
         'status': {
             'type': 'string',
             'required': true,
diff --git a/models/Exports.server.bones b/models/Exports.server.bones
index 3520af7..9926b19 100644
--- a/models/Exports.server.bones
+++ b/models/Exports.server.bones
@@ -1,11 +1,14 @@
-var Step = require('step'),
-    Queue = require('../lib/queue'),
-    fs = require('fs'),
-    path = require('path'),
-    exec = require('child_process').exec,
-    spawn = require('child_process').spawn,
-    settings = Bones.plugin.config,
-    pids = {};
+var Step = require('step');
+var Queue = require('../lib/queue');
+var fs = require('fs');
+var path = require('path');
+var exec = require('child_process').exec;
+var spawn = require('child_process').spawn;
+var settings = Bones.plugin.config;
+var pids = {};
+var pid_errors = {};
+var crashutil = require('../lib/crashutil');
+var redirect = require('../lib/redirect.js');
 
 var queue = new Queue(start, 1);
 function start(id, callback) {
@@ -19,7 +22,7 @@ function start(id, callback) {
 
         var args = [];
         // tilemill index.js
-        args.push(path.resolve(path.join(__dirname + '/../index.js')));
+        args.push(path.resolve(path.join(__dirname, '../index.js')));
         // export command
         args.push('export');
         // datasource
@@ -29,31 +32,57 @@ function start(id, callback) {
         // format, don't try to guess extension based on filepath
         args.push('--format=' + data.format);
         // url
-        args.push('--url=http://'+settings.coreUrl+'/api/Export/'+data.id);
+        args.push('--url=http://' + settings.coreUrl + '/api/Export/'+data.id);
         // Log crashes to output directory.
         args.push('--log=1');
+        // Don't output progress information.
+        args.push('--quiet');
 
         if (data.bbox) args.push('--bbox=' + data.bbox.join(','));
         if (data.width) args.push('--width=' + data.width);
         if (data.height) args.push('--height=' + data.height);
         if (!_(data.minzoom).isUndefined()) args.push('--minzoom=' + data.minzoom);
         if (!_(data.maxzoom).isUndefined()) args.push('--maxzoom=' + data.maxzoom);
+        if (!_(data.metatile).isUndefined()) args.push('--metatile=' + data.metatile);
 
         var child = spawn(process.execPath, args, {
             env: _(process.env).extend({
                 tilemillConfig:JSON.stringify(settings)
             }),
             cwd: undefined,
-            customFds: [-1, -1, -1],
             setsid: false
         });
+
+        redirect.onData(child);
+
         var pid = child.pid;
         pids[pid] = true;
 
-        child.on('exit', function(code) {
-            delete pids[pid];
-            callback();
+        child.on('exit', function(code, signal) {
+            if (code !== 0) {
+                var message = 'Export process failed';
+                if (signal) {
+                    message += " with signal '" + signal + "' ";
+                }
+                console.warn(message);
+                message += " (see tilemill log for details)"
+                pid_errors[pid] = message;
+                crashutil.display_crash_log(function(err,logname) {
+                    if (err) {
+                        console.warn(err.stack || err.toString());
+                    }
+                    if (logname) {
+                        console.warn("Please post this crash log: '" + logname + "' to https://github.com/mapbox/tilemill/issues");
+                    }
+                    delete pids[pid];
+                    callback();
+                });
+            } else {
+                delete pids[pid];
+                callback();
+            }
         });
+
         (new models.Export(data)).save({
             pid:pid,
             created:Date.now(),
@@ -68,8 +97,16 @@ function start(id, callback) {
 // 2. when reading models the process health is checked. if the pid
 //    is not found, the model's status should be updated.
 function check(data) {
-    if (data.status === 'processing' && data.pid && !pids[data.pid])
-        return { status: 'error', error: 'Export process died' };
+    if (data.status === 'processing' && data.pid && !pids[data.pid]) {
+        var attr = { status: 'error' };
+        if (pid_errors[data.pid]) {
+            attr.error = String(pid_errors[data.pid]);
+            delete pid_errors[data.pid];
+        } else {
+            attr.error = 'Export process died'
+        }
+        return attr;
+    }
     if (data.status === 'waiting' && !_(queue.queue).include(data.id))
         queue.add(data.id);
 };
diff --git a/models/Layer.bones b/models/Layer.bones
index c874636..5ee3ba6 100644
--- a/models/Layer.bones
+++ b/models/Layer.bones
@@ -32,6 +32,10 @@ model.prototype.schema = {
         'srs': {
             'type': 'string'
         },
+        'status': {
+            'type': 'string',
+            'enum': ['on', 'off']
+        },
         'geometry': {
             'type': 'string',
             'enum': ['polygon', 'multipolygon', 'point', 'multipoint', 'linestring', 'multilinestring', 'raster', 'unknown']
@@ -39,6 +43,9 @@ model.prototype.schema = {
         'Datasource': {
             'type': 'object',
             'required': true
+        },
+        'advanced': {
+            'type': 'object'
         }
     }
 };
@@ -92,6 +99,7 @@ model.prototype.validateAsync = function(attributes, options) {
     (new models.Datasource(attr)).fetch({
         success: _(function(model, resp) {
             if (resp.geometry_type) this.set({geometry:resp.geometry_type});
+            this.set({extent: resp.unproj_extent});
             options.success(model, resp);
         }).bind(this),
         error: options.error
diff --git a/models/Library.server.bones b/models/Library.server.bones
index 8dbc9d9..e430634 100644
--- a/models/Library.server.bones
+++ b/models/Library.server.bones
@@ -4,20 +4,27 @@ var path = require('path');
 var read = require('../lib/fsutil').read;
 var readdir = require('../lib/fsutil').readdir;
 var mkdirp = require('../lib/fsutil').mkdirp;
+var winDrives = require('../lib/fsutil').winDrives;
 var rm = require('../lib/fsutil').rm;
 var s3 = require('../lib/s3');
 var config = Bones.plugin.config;
 var url = require('url');
+// node v6 -> v8 compatibility
+var existsAsync = require('fs').exists || require('path').exists;
+var millstone = require('millstone');
 
-// Extensions supported by TileMill. See `millstone.resolve()` for
-// the source of this list.
-var extFile = [
-    '.zip', '.shp', '.png', '.geotiff', '.geotif', '.tiff',
-    '.tif', '.vrt', '.kml', '.geojson', '.json', '.rss',
-    '.csv', '.tsv', '.txt'
-];
+
+// File based extensions supported by TileMill.
+var extFile = [];
 // Sqlite extensions.
-var extSqlite = [ '.sqlite', '.db', '.sqlite3', '.spatialite' ];
+var extSqlite = [];
+Object.keys(millstone.valid_ds_extensions).forEach(function(i){
+    if (millstone.valid_ds_extensions[i] == 'sqlite') {
+         extSqlite.push(i);
+    } else {
+         extFile.push(i);
+    }
+});
 
 var formatFileSize = function(size) {
     var size = parseInt(size), scaled, suffix;
@@ -41,24 +48,58 @@ var formatFileSize = function(size) {
 models.Library.prototype.sync = function(method, model, success, error) {
     if (method !== 'read') return error(new Error('Method not supported.'));
 
+
     switch (model.id) {
     case 'file':
     case 'sqlite':
+
+        function isRelative(loc) {
+            if (process.platform === 'win32') {
+                return loc[0] !== '\\' && loc[0] !== '/' && loc.match(/^[a-zA-Z]:/) === null;
+            } else {
+                return loc[0] !== '/';
+            }
+        }
+
         var sep = process.platform === 'win32' ? '\\' : '/';
-        var location = (model.get('location') || process.env.HOME)
-            .replace(/^([a-zA-Z]:\\|\/)/, sep);
+        var location = (model.get('location') || process.env.HOME);
+        location = location.replace(/^~/, process.env.HOME);
+        if (process.platform === 'win32') {
+            //https://github.com/mapbox/tilemill/issues/1679
+            location = location.replace(/^\\([a-zA-Z]:)/, "$1");
+            if (location == '/') {
+                var data = {};
+                data.id = model.id;
+                data.location = location;
+                data.assets = _(winDrives()).chain()
+                    .map(function(f) {
+                        var asset = { name: f };
+                        asset.location = f;
+                        return asset;
+                    })
+                    .sortBy(function(f) {return f.name.toLowerCase(); })
+                    .value();
+                return success(data);
+            }
+        }
+        location = location.replace(/^~/, process.env.HOME);
 
         // Resolve paths relative to project directory.
-        if (!location[0] === sep) {
+        if (isRelative(location)) {
+            if (process.env.NODE_ENV === 'development') console.log('[tilemill] [library] detected relative path: ' + location);
             location = path.join(config.files, 'project', model.get('project'), location);
         }
 
-        path.exists(location, function(exists) {
-            if (!exists) location = process.env.HOME;
+        existsAsync(location, function(exists) {
+            if (!exists) {
+                if (process.env.NODE_ENV === 'development') console.log('[tilemill] [library] path ' + location + ' not found defaulting to home directory ' + process.env.HOME);
+                location = process.env.HOME;
+            }
             readdir(location, function(err, files) {
                 if (err &&
                     err.code !== 'EACCES' &&
-                    err.code !== 'UNKNOWN') return error(err);
+                    err.code !== 'UNKNOWN' &&
+                    err.code !== 'EPERM') return error(err);
                 var data = {};
                 var ext = model.id === 'file' ? extFile : extSqlite;
                 data.id = model.id;
@@ -67,10 +108,6 @@ models.Library.prototype.sync = function(method, model, success, error) {
                     .reject(function(f) { return f.basename[0] === '.'; })
                     // Reject Icon? files from Mac OS X. See #917.
                     .reject(function(f) { return f.basename === 'Icon\r'; })
-                    .sortBy(function(f) {
-                        var pre = f.isDirectory() ? 0 : 1;
-                        return pre + f.basename;
-                    })
                     .map(function(f) {
                         var asset = { name: f.basename };
                         var filepath = path.join(location, f.basename);
@@ -83,6 +120,7 @@ models.Library.prototype.sync = function(method, model, success, error) {
                         }
                     })
                     .compact()
+                    .sortBy(function(f) {return f.name.toLowerCase(); })
                     .value()
                 return success(data);
             });
@@ -98,6 +136,7 @@ models.Library.prototype.sync = function(method, model, success, error) {
         data.assets = [];
         options.bucket = 'mapbox-geodata';
         options.prefix = data.location;
+        options.proxy = Bones.plugin.config.httpProxy;
         s3.list(options, function(err, objects) {
             if (err) return error(err);
             data.assets = _(objects).chain()
diff --git a/models/Plugin.server.bones b/models/Plugin.server.bones
index 36e857e..a96e22e 100644
--- a/models/Plugin.server.bones
+++ b/models/Plugin.server.bones
@@ -4,10 +4,12 @@ var Step = require('step');
 var semver = require('semver');
 
 models.Plugin.prototype.sync = function(method, model, success, error) {
+    var opts = {};
+    if (Bones.plugin.config.httpProxy) opts.proxy = Bones.plugin.config.httpProxy;
     // Deletion is a special case. We don't need to validate
     // version, package info.
     if (method === 'delete') return Step(function() {
-        npm.load({}, this);
+        npm.load(opts, this);
     }, function(err) {
         if (err) throw err;
         npm.localPrefix = path.join(process.env.HOME, '.tilemill');
@@ -22,7 +24,7 @@ models.Plugin.prototype.sync = function(method, model, success, error) {
 
     var version = Bones.plugin.abilities.tilemill.version;
     Step(function() {
-        npm.load({}, this);
+        npm.load(opts, this);
     }, function(err) {
         if (err) throw err;
         npm.localPrefix = path.join(process.env.HOME, '.tilemill');
diff --git a/models/Plugins.server.bones b/models/Plugins.server.bones
index 10ad137..ce77e82 100644
--- a/models/Plugins.server.bones
+++ b/models/Plugins.server.bones
@@ -1,12 +1,15 @@
 var npm = require('npm');
 var path = require('path');
 var Step = require('step');
+var semver = require('semver');
 
 models.Plugins.prototype.sync = function(method, model, success, error) {
     if (method !== 'read') return error(new Error('Unsupported method.'));
 
     Step(function() {
-        npm.load({}, this);
+        var opts = {};
+        if (Bones.plugin.config.httpProxy) opts.proxy = Bones.plugin.config.httpProxy;
+        npm.load(opts, this);
     }, function(err) {
         if (err) throw err;
 
@@ -19,7 +22,7 @@ models.Plugins.prototype.sync = function(method, model, success, error) {
 
         var group = this.group();
         _(resp).each(function(data) {
-            npm.commands.view([data.name], true, group());
+            npm.commands.view([data.name + '@*'], true, group());
         });
     }, function(err, resp) {
         if (err) return error(err);
@@ -27,8 +30,14 @@ models.Plugins.prototype.sync = function(method, model, success, error) {
         // - Copy 'name' to 'id' for Backbone.
         // - Filters packages to ones with tilemill as an engine
         success(resp
-            .map(function(p) { for (var key in p) return p[key] })
-            .filter(function(p) { return p.engines && p.engines.tilemill })
+            .map(function(p) {
+                var keys = _.keys(p).reverse();
+                for (var i in keys) {
+                    if (p[keys[i]].engines && p[keys[i]].engines.tilemill &&
+                        semver.satisfies(Bones.plugin.abilities.tilemill.version, p[keys[i]].engines.tilemill))
+                        return p[keys[i]]
+                }})
+            .filter(function(p) { return p})
             .map(function(p) { p.id = p.name; return p; }));
     });
 };
diff --git a/models/Preview.server.bones b/models/Preview.server.bones
index 47870f6..e05f9fc 100644
--- a/models/Preview.server.bones
+++ b/models/Preview.server.bones
@@ -11,6 +11,7 @@ models.Preview.prototype.sync = function(method, model, success, error) {
         source.getInfo(function(err, info) {
             if (err) return error(err);
             info.tiles = ['http://' + settings.tileUrl + '/tile/' + model.id + '/{z}/{x}/{y}.png?' + (+new Date)];
+            info.grids = ['http://' + settings.tileUrl + '/tile/' + model.id + '/{z}/{x}/{y}.grid.json?' + (+new Date)];
             success(_(info).extend({id: model.id }));
         });
     });
diff --git a/models/Project.bones b/models/Project.bones
index 31ebc61..7c0d12c 100644
--- a/models/Project.bones
+++ b/models/Project.bones
@@ -21,6 +21,14 @@ model.prototype.schema = {
             'type': ['object', 'array'],
             'required': true
         },
+        'scale': {
+            'type': 'float',
+            'required': true
+        },
+        'metatile': {
+            'type': 'integer',
+            'required': true
+        },
 
         // TileMill-specific properties. @TODO these need a home, see
         // https://github.com/mapbox/tilelive-mapnik/issues/4
@@ -169,7 +177,9 @@ model.prototype.defaults = {
     'srs': '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 '
         + '+lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over',
     'Stylesheet': [],
-    'Layer': []
+    'Layer': [],
+    'scale': 1,
+    'metatile': 2
 };
 
 // Set model options to `this.options`.
@@ -187,7 +197,7 @@ model.prototype.setDefaults = function(data) {
         !this.get('Stylesheet').length && (template.Stylesheet = this.STYLESHEET_DEFAULT);
         !this.get('Layer').length && (template.Layer = this.LAYER_DEFAULT);
         template.minzoom = 0;
-        template.maxzoom = 8;
+        template.maxzoom = 22;
     }
     else {
         !this.get('Stylesheet').length && (template.Stylesheet = this.STYLESHEET_DEFAULT_NODATA);
@@ -288,6 +298,30 @@ model.prototype.poll = function(options) {
     });
 };
 
+// Hit the project poll endpoint for the tileserver instance
+model.prototype.pollTileServer = function(options) {
+    if (Bones.server) throw Error('Client-side method only.');
+    if (this.get('tiles') && this.get('tiles').length) {
+        var tiles_url = this.get('tiles')[0];
+        var project_status_url = tiles_url.slice(0,tiles_url.indexOf('{z}')) + 'project-status';
+        $.ajax({
+            url: project_status_url,
+            type: 'GET',
+            contentType: 'application/json',
+            processData: false,
+            success: _(function(resp) {
+                resp.status_url = project_status_url;
+                if (options.success) options.success(this, resp);
+            }).bind(this),
+            error: _(function(resp) {
+                if (options.error) options.error(this, resp);
+            }).bind(this)
+        });
+    } else {
+        options.success(this, {});
+    }
+};
+
 // Hit the project flush endpoint.
 model.prototype.flush = function(layer, url, options) {
     if (Bones.server) throw Error('Client-side method only.');
diff --git a/models/Project.server.bones b/models/Project.server.bones
index d4014fd..b92a031 100644
--- a/models/Project.server.bones
+++ b/models/Project.server.bones
@@ -13,6 +13,10 @@ var settings = Bones.plugin.config;
 var tileURL = _('http://<%=url%>/tile/<%=id%>/{z}/{x}/{y}.<%=format%>?updated=<%=updated%>').template();
 var request = require('request');
 
+// object tracks status of tileserver's status localizing a project
+// key:model.id value:friendly message about activity or ''
+var project_tile_status = {};
+
 // Project
 // -------
 // Implement custom sync method for Project model. Writes projects to
@@ -33,11 +37,18 @@ models.Project.prototype.sync = function(method, model, success, error) {
         break;
     case 'create':
     case 'update':
+        if (method == 'update') {
+            // clear mapnik's global cache of markers and shapefiles
+            mapnik.clearCache();
+        }
+        delete project_tile_status[model.id];
         saveProject(model, function(err, model) {
             return err ? error(err) : success(model);
         });
         break;
     case 'delete':
+        mapnik.clearCache();
+        delete project_tile_status[model.id];
         destroyProject(model, function(err) {
             return err ? error(err) : success({});
         });
@@ -57,6 +68,10 @@ models.Project.prototype.sync = function(method, model, success, error) {
             }
         });
         break;
+    // simple status poll, designed to get tileserver instance status
+    case 'status':
+        success({status:project_tile_status[model.id]});
+        break;
     // Custom sync method to flush the cache for a specific model. Requires
     // `model.options.layer` to be set.
     case 'flush':
@@ -161,11 +176,13 @@ function loadProject(model, callback) {
         });
     },
     function(err, file) {
-        if (err) return callback(new Error.HTTP('Project does not exist', 404));
+        var projectName = path.join(modelPath, 'project.mml');
+        if (err) return callback(new Error.HTTP('Project does not exist: "' + projectName + '"', 404));
         try {
             object = _(object).extend(JSON.parse(file.data));
         } catch(err) {
-            throw err;
+            var err_message = 'Could not open project.mml file for "' + model.id + '". Error was: \n\n"' + err.message + '"\n\n(in ' + projectName + ')';
+            return callback(new Error(err_message));
         }
 
         object.id = model.id;
@@ -233,8 +250,8 @@ function loadProject(model, callback) {
             format: 'grid.json',
             updated: object._updated
         })];
+        object.template = template(object.interactivity);
         if (object.interactivity) {
-            object.template = template(object.interactivity);
             object.interactivity.fields = fields(object);
         }
         this();
@@ -259,10 +276,11 @@ function loadProjectAll(model, callback) {
         if (files.length === 0) return this(null, []);
         var group = this.group();
         _(files).chain()
-            .filter(function(file) { return file.isDirectory() })
+            .filter(function(file) { return (file.isDirectory() && file.basename[0] !== '.');  })
             .each(function(file) { loadProject({id:file.basename}, group()) });
     },
     function(err, models) {
+        if (err && process.env.NODE_ENV === 'development') console.log('[tilemill] skipped loading project: ' + err.stack || err.toString());
         // Ignore errors from loading individual models (e.g.
         // don't let one bad apple spoil the collection).
         models = _(models).chain()
@@ -276,11 +294,14 @@ function loadProjectAll(model, callback) {
 // Destroy a project. `rm -rf` equivalent for the project directory.
 function destroyProject(model, callback) {
     var modelPath = path.resolve(path.join(settings.files, 'project', model.id));
-    // Workaround to access denied error on Windows when mapnik has
-    // open file handles to a data file in a project that needs to
-    // be deleted. Stopgap is to kill the tileserver, delete the project
-    // and let the tileserver start back up on its own.
-    if (process.platform === 'win32') {
+	if (process.platform === 'win32') {
+        // https://github.com/mapbox/tilemill/issues/1121
+        // Workaround to access denied error on Windows when mapnik has
+        // open file handles to a data file in a project that needs to
+        // be deleted. Stopgap is to kill the tileserver, delete the project
+        // and let the tileserver start back up on its own.
+        // NOTE: not needed with symlinks but millstone does not always use symlinks
+        // so we need this on win32 as per https://github.com/mapbox/millstone/issues/71
         request.post({ url:'http://'+settings.tileUrl+'/restart' }, function(err) {
             rm(modelPath, callback);
         });
@@ -310,6 +331,18 @@ function saveProject(model, callback) {
             return s.id || s;
         });
 
+        data.Layer = _(data.Layer).map(function(l) {
+            if (l.Datasource.file) {
+                l.Datasource.file = l.Datasource.file.trim();
+                if (!l.Datasource.file.indexOf(modelPath)) {
+                    l.Datasource.file = path.relative(modelPath, l.Datasource.file);
+                } else {
+                    l.Datasource.file = l.Datasource.file.replace(/^~/, process.env.HOME);
+                }
+            }
+            return l;
+        });
+
         _(data).chain()
             .keys()
             .filter(function(k) { return schema[k] && !schema[k].ignore })
@@ -346,9 +379,7 @@ function saveProject(model, callback) {
             _updated: updated,
             tiles: [tiles],
             grids: [grids],
-            template: model.get('interactivity')
-                ? template(model.get('interactivity'))
-                : undefined
+            template: template(model.get('interactivity'))
         });
 
         if (err) throw err;
@@ -382,6 +413,7 @@ function compileStylesheet(mml, callback) {
     var fonts = styles.match(/font-directory:[\s]*url\(['"]*([^'"\)]*)['"]*\)/);
     if (fonts) {
         fonts = fonts[1];
+        // @TODO - will be broken on windows
         fonts = fonts.charAt(0) !== '/'
             ? path.join(settings.files, 'project', mml.id, fonts)
             : fonts;
@@ -397,10 +429,19 @@ function compileStylesheet(mml, callback) {
     // Hard clone the model JSON to avoid any alterations to it.
     // @TODO: is this necessary?
     var data = JSON.parse(JSON.stringify(mml));
-    new carto.Renderer(env).render(data, function(err, output) {
-        if (err) callback(err);
-        else callback(null, output);
-    });
+    // try/catch here as per https://github.com/mapbox/tilemill/issues/1370
+    try {
+        new carto.Renderer(env).render(data, function(err, output) {
+            if (err) {
+                callback(err);
+            } else {
+                callback(null, output);
+            }
+        });
+    } catch (err) {
+        if (process.env.NODE_ENV === 'development') console.log('[tilemill] Error compiling CartoCSS: ' + err.stack || err.toString());
+        callback(err);
+    }
 }
 
 var localizedCache = {};
@@ -441,24 +482,40 @@ models.Project.prototype.localize = function(mml, callback) {
 
     var localizeTime;
     var compileTime;
+    var resolveInterval = {};
     Step(function() {
         localizeTime = (+new Date);
+        project_tile_status[model.id] = 'patience, loading project';
+        setInterval(function() {
+            if (millstone.downloads) {
+                var num_downloads = Object.keys(millstone.downloads).length;
+                if (num_downloads) {
+                    project_tile_status[model.id] = 'caching ' + num_downloads + ' resource' + (num_downloads > 1 ? 's' : '');
+                }
+            }
+        },1000);
         millstone.resolve({
             mml: mml,
             base: path.join(settings.files, 'project', model.id),
             cache: path.join(settings.files, 'cache')
         }, this);
     }, function(err, localized) {
-        if (err) throw err;
+        clearInterval(resolveInterval);
+        if (err) {
+            delete project_tile_status[model.id];
+            throw err;
+        }
 
         localizedCache[key].debug.localize = (+new Date) - localizeTime + 'ms';
         localizedCache[key].mml = localized;
 
         compileTime = (+new Date);
+        project_tile_status[model.id] = 'compiling css';
         compileStylesheet(localized, this);
     }, function(err, compiled) {
+        // clear the status, indicating project is finished loading
+        delete project_tile_status[model.id];
         if (err) throw err;
-
         localizedCache[key].debug.compile = (+new Date) - compileTime + 'ms';
         localizedCache[key].xml = compiled;
         localizedCache[key].emit('load');
@@ -480,7 +537,7 @@ function fields(opts) {
     // Determine fields that need to be included from templates.
     // @TODO allow non-templated fields to be included.
     var fields = [full, teaser, location]
-        .join(' ').match(/\{\{#?\/?\^?([\w\d\s-:]+)\}\}/g);
+        .join(' ').match(/\{\{#?\/?\^?([\w\d\s-:]+)\}\}/g) || [];
 
     // Include `key_field` for PostGIS Layers.
     var layer = opts.interactivity.layer;
@@ -501,7 +558,9 @@ function fields(opts) {
 
 // Generate combined template from templates.
 function template(opts) {
-    opts = opts || {};
+    if (!opts || !opts.layer || (!opts.template_teaser && !opts.template_full && !opts.template_location))
+        return "";
+
     return '{{#__location__}}' + (opts.template_location || '') + '{{/__location__}}' +
         '{{#__teaser__}}' + (opts.template_teaser || '') + '{{/__teaser__}}' +
         '{{#__full__}}' + (opts.template_full || '') + '{{/__full__}}';
diff --git a/package.json b/package.json
index 06234ef..8f30e37 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,10 @@
 {
     "name": "tilemill",
-    "version": "0.9.0",
+    "version": "0.10.1",
     "main": "./index.js",
     "description": "A modern map design studio.",
     "keywords": ["map", "design", "cartography"],
-    "url": "http://github.com/developmentseed/tilemill",
+    "url": "http://github.com/mapbox/tilemill",
     "repository": {
         "type": "git",
         "url": "git://github.com/mapbox/tilemill.git"
@@ -17,47 +17,52 @@
         "Dmitri Gaskin <dmitrig01>",
         "AJ Ashton <ajashton>",
         "Tristen Brown <tristen>",
-        "Young Hahn <yhahn>"
+        "Young Hahn <yhahn>",
+        "Ansis Brammanis <aibram>"
     ],
     "licenses": [{ "type": "BSD" }],
     "dependencies": {
-        "backbone-dirty": "1.1.x",
-        "bones": "~1.3.20",
+        "glob":"~3.1.9",
+        "generic-pool": "~2.0.0",
+        "backbone-dirty": "~1.1.3",
+        "bones": "~1.3.27",
         "chrono": "~1.0.1",
         "JSV": "3.5.x",
-        "mbtiles": "~0.1.2",
         "sax": "0.1.x",
-        "request": "1.9.x",
-        "step": "0.0.x",
-        "tilelive": "~4.0.3",
-        "mapnik": "~0.5.16",
-        "millstone": "~0.3.0",
-        "sphericalmercator": "~1.0.1",
-        "tilelive-mapnik": "~0.2.0",
-        "underscore": "1.1.x",
-        "carto": "~0.4.3",
-        "wax": "5.0.0-alpha2",
+        "request": "~2.9.153",
+        "step": "~0.0.5",
+        "mapnik": "~0.7.15",
+        "tilelive": "~4.3.0",
+        "mbtiles": "~0.2.6",
+        "tilelive-mapnik": "~0.4.4",
+        "carto": "0.9.3",
+        "millstone": "~0.5.11",
+        "sphericalmercator": "~1.0.2",
+        "underscore": "~1.3.3",
+        "wax": "6.4.2",
         "node-markdown": "0.1.0",
-        "sqlite3": "~2.0.16",
+        "sqlite3": "~2.1.5",
         "passport": "0.1.x",
         "passport-oauth": "0.1.x",
-        "modestmaps": "1.0.0-alpha",
-        "npm": "1.0.x",
+        "modestmaps": "3.3.5",
+        "npm": "~1.1.50",
         "semver": "1.0.x",
-        "optimist": "0.1.x",
-        "topcube": "https://github.com/creationix/topcube/tarball/v0.1.1",
-        "mkdirp": "~0.2.1"
+        "optimist": "~0.3.1",
+        "topcube": "0.1.3",
+        "mkdirp": "~0.3.3"
     },
     "devDependencies": {
-        "jshint": "0.2.x"
+        "jshint": "0.2.x",
+        "mocha": "*",
+        "difflet": "0.2.x"
     },
     "scripts": {
         "start": "./index.js",
-        "test": "expresso",
-        "postinstall": "GITPATH=/usr/bin:/usr/local/bin:/usr/local/git/bin\nPATH=$PATH:$GITPATH; export PATH\n\nif [ -z $( which git ) ]; then\n  echo \"Unable to find git binary in \\$GITPATH\"\n  exit 1\nfi\n\nSHORT_VERSION=$( git describe --tags )\nVERSION=$( echo $SHORT_VERSION | sed -e 's/^v//' | sed -e 's/-/./' | sed -e 's/-.*//' )\n\necho \"$SHORT_VERSION\n$VERSION\" > VERSION"
+        "test": "mocha --ignore-leaks --timeout 10000",
+        "postinstall": "node ./lib/gitutil.js"
     },
     "bin": {
         "tilemill": "./index.js"
     },
-    "engines": { "node": "0.4.x" }
+    "engines": { "node": "0.6.x || 0.8.x" }
 }
diff --git a/plugins/carto/templates/Reference._ b/plugins/carto/templates/Reference._
index 607496c..eeb02b6 100644
--- a/plugins/carto/templates/Reference._
+++ b/plugins/carto/templates/Reference._
@@ -13,11 +13,11 @@
 
 <h2>Need help?</h2>
 
-<p>Read the <a target='_blank' href="/#!/manual">manual</a> for a full guide to using TileMill.</p>
+<p>Read the <a href="/#!/manual">manual</a> for a full guide to using TileMill.</p>
 
 <h2>Selectors</h2>
 
-<p><a href='http://www.tilemill.com'>TileMill</a> supports the <a href="https://github.com/mapbox/carto">carto</a> map styling language. It should be familiar to CSS users and easy to pick up for everyone else.</p>
+<p><a target='_blank' href='http://www.tilemill.com'>TileMill</a> supports the <a target='_blank' href="https://github.com/mapbox/carto">carto</a> map styling language. It should be familiar to CSS users and easy to pick up for everyone else.</p>
 
 <p>A simple carto style looks like</p>
 
@@ -100,12 +100,18 @@
 <p>Different properties in carto accept different types of values - colors, dimensions, and more.</p>
 
 <h4>Colors</h4>
+
+<p>Carto accepts a variety of syntaxes for colors - HTML-style hex values,
+rgb, rgba, and hsl. It also supports the predefined HTML colors names, like
+<code>yellow</code> and <code>blue</code>.</p>
+
 <pre class='carto-snippet'>
 #line {
   line-color: #ff0;
   line-color: #ffff00;
   line-color: rgb(255, 255, 0);
   line-color: rgba(255, 255, 0, 1);
+  line-color: hsl(100, 50%, 50%);
   line-color: yellow;
 }</pre>
 
@@ -144,7 +150,7 @@ Map { background-color: @green; }
 
 <h2>Color functions</h2>
 
-<p>Carto inherits color manipulation functions from <a href='http://lesscss.org' target='_blank'>less.js</a>.</p>
+<p>Carto inherits color manipulation functions from <a target='_blank' href='http://lesscss.org' target='_blank'>less.js</a>.</p>
 
 <pre class='carto-snippet'>
 // lighten or darken a color by 10%
diff --git a/plugins/editor/views/Stylesheet.bones b/plugins/editor/views/Stylesheet.bones
index 4798787..293ee70 100644
--- a/plugins/editor/views/Stylesheet.bones
+++ b/plugins/editor/views/Stylesheet.bones
@@ -19,7 +19,11 @@ view.prototype.render = function() {
 
 view.prototype.save = function() {
     var attr = Bones.utils.form(this.$('.form'), this.model);
-    var options = { error: function(m, e) { new views.Modal(e); } };
+    var options = { error: function(m, resp) {
+            console.log('error saving project: ' + m.id);
+            new views.Modal(resp);
+        }
+    };
     if (this.model.set(attr, options)) {
         this.model.collection.add(this.model);
         this.$('.close').click();
diff --git a/plugins/editor/views/Stylesheets.bones b/plugins/editor/views/Stylesheets.bones
index 12bc204..57e796f 100644
--- a/plugins/editor/views/Stylesheets.bones
+++ b/plugins/editor/views/Stylesheets.bones
@@ -69,24 +69,41 @@ view.prototype.save = function() {
 //
 // and highlight the line number and stylesheet appropriately if
 // found. Otherwise, display error in a modal.
-view.prototype.error = function(model, err) {
-    if (err.responseText) err = JSON.parse(err.responseText).message;
-    var err = _(err.toString().split('\n')).compact();
-    for (var i = 0; i < err.length; i++) {
-        var match = err[i].match(/^(Error: )?([\w.]+):([\d]+):([\d]+) (.*)$/);
-        if (match) {
-            var stylesheet = this.model.get('Stylesheet').get(match[2]),
-                id = 'stylesheet-' + stylesheet.id.replace(/[\.]/g, '-'),
-                lineNum = parseInt(match[3]) - 1;
-
-            this.$('.tabs a[href=#' + id + ']').addClass('error');
-            stylesheet.errors = stylesheet.errors || [];
-            stylesheet.errors[lineNum] = match[5];
-            stylesheet.codemirror.setMarker(lineNum, '%N%', 'error');
-        } else {
-            new views.Modal(err[i]);
-            break;
+view.prototype.error = function(model, resp) {
+    if (resp.responseText) {
+        // this assume Carto.js specific error array format response
+        var err_message = JSON.parse(resp.responseText).message;
+        var err_group = _(err_message.toString().split('\n')).compact();
+        var lines = []
+        for (var i = 0; i < err_group.length; i++) {
+            var match = err_group[i].match(/^(Error: )?([\w.]+):([\d]+):([\d]+) (.*)$/);
+            if (match) {
+                var stylesheet = this.model.get('Stylesheet').get(match[2]),
+                    id = 'stylesheet-' + stylesheet.id.replace(/[\.]/g, '-'),
+                    lineNum = parseInt(match[3]) - 1;
+                this.$('.tabs a[href=#' + id + ']').addClass('error');
+                stylesheet.errors = stylesheet.errors || [];
+                lines.push(lineNum+1);
+                stylesheet.errors[lineNum] = match[5] + ' (line ' + (lineNum+1) + ')';
+                stylesheet.codemirror.setMarker(lineNum, '%N%', 'error');
+                if (err_group.length == 1) {
+                    this.$('.status').addClass('active');
+                    this.$('.status .content').text(stylesheet.errors[lineNum]);
+                }
+            } else {
+                new views.Modal(err_group[i]);
+                break;
+            }
+            if (lines.length > 1) {
+                this.$('.status').addClass('active');
+                this.$('.status .content').text("Click lines " + lines + " to see each error");
+            }
         }
+    } else {
+        // will hit this if the server is offline and the user tries to save
+        // We attach a error message to this resp object so that that the Modal can display it
+        resp.err_message = 'Could not save project "' + model.id + '"';
+        new views.Modal(resp);
     }
 };
 
@@ -117,7 +134,7 @@ view.prototype.makeStylesheet = function(model) {
             _.debounce(self.colors, 500);
         },
         onGutterClick: _(function(editor, line, ev) {
-            if (model.errors[line]) {
+            if (model.errors && model.errors[line]) {
                 this.$('.status').addClass('active');
                 this.$('.status .content').text(model.errors[line]);
                 return false;
diff --git a/plugins/map/package.json b/plugins/map/package.json
deleted file mode 100644
index 97f92a1..0000000
--- a/plugins/map/package.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-    "name": "map",
-    "description": "Live map preview.",
-    "engines": {"tilemill":"*"},
-    "private": true
-}
diff --git a/plugins/templates/templates/Templates._ b/plugins/templates/templates/Templates._
index 9e81540..a07e8dd 100644
--- a/plugins/templates/templates/Templates._
+++ b/plugins/templates/templates/Templates._
@@ -43,7 +43,7 @@
           <option <% if (interactivity.layer === layer.get('id')) { %>selected='selected'<% } %> value='<%= layer.get('id') %>'><%= layer.get('id') %></option>
         <% }); %>
       </select>
-      <small>Layer to use for interaction data</small>
+      <span id="layer-info"><small>Layer to use for interaction data</small></span>
     </div>
     <div class='templates-tokens code tokens scrolling'></div>
     <div class='description'><small>
diff --git a/plugins/templates/views/Templates.bones b/plugins/templates/views/Templates.bones
index 99d2928..d0f5419 100644
--- a/plugins/templates/views/Templates.bones
+++ b/plugins/templates/views/Templates.bones
@@ -27,6 +27,8 @@ view.prototype.attach = function() {
     if (!layer) {
         this.$('.tokens').empty();
         this.$('.requires-tokens').attr('disabled', true);
+        this.$('.description.toggler').removeClass('warning');
+        this.$('#layer-info').html('<small>Layer to use for interaction data</small>');
         return true;
     }
 
@@ -39,19 +41,25 @@ view.prototype.attach = function() {
         $(this.el).removeClass('loading').removeClass('restartable');
     }).bind(this);
 
+    var layer_attr = layer.get('Datasource');
+    if (layer_attr.type === 'postgis' && !layer_attr.key_field) {
+        this.$('#layer-info').html('<span class="warning-text"><strong>Warning</strong>: This PostGIS layer does not have a <strong>Unique key field</strong> defined. This is required for valid MBTiles exports.</span>');
+        this.$('.description.toggler').addClass('warning');
+    } else {
+        this.$('#layer-info').html("<small>Layer to use for interaction data</small>");
+        this.$('.description.toggler').removeClass('warning');
+    }
+
     // Cache the datasource model to `this.datasource` so it can
     // be used to live render/preview the formatters.
     if (!this.datasource || this.datasource.id !== layer.get('id')) {
         $(this.el).addClass('loading').addClass('restartable');
-        var attr = _(layer.get('Datasource')).chain()
+        var attr = _(layer_attr).chain()
             .clone()
             .extend({
                 id: layer.get('id'),
                 project: this.model.get('id'),
-                // millstone will not allow `srs` be undefined for inspection so we set
-                // it to null. We could use the layer's SRS, but this likely has fewer
-                // side effects.
-                srs: null
+                srs: layer.get('srs')
             })
             .value();
         this.datasource = new models.Datasource(attr);
diff --git a/servers/App.bones b/servers/App.bones
index ea41c06..600d3a7 100644
--- a/servers/App.bones
+++ b/servers/App.bones
@@ -1,5 +1,6 @@
 var fs = require('fs');
 var path = require('path');
+var Step = require('step');
 var env = process.env.NODE_ENV || 'development';
 
 server = Bones.Server.extend({});
@@ -26,6 +27,9 @@ server.prototype.initialize = function(app) {
     // Used by the native Cocoa app to retrieve specific settings.
     this.get('/api/Key/:key', this.getKey);
 
+    // Endpoint for checking for new TileMill version
+    this.get('/api/updatesVersion', this.updatesVersion);
+
     // Custom Project sync endpoints.
     this.get('/api/Project/:id.xml', this.projectXML);
     this.get('/api/Project/:id.debug', this.projectDebug);
@@ -80,7 +84,9 @@ server.prototype.projectXML = function(req, res, next) {
     model.fetch({
         success: function(model, resp) {
             model.localize(model.toJSON(), function(err) {
-                if (err) return next(err);
+                if (err) return next({"message":err.message});
+                // https://github.com/mapbox/tilemill/issues/1421
+                res.header('Content-Disposition', 'attachment');
                 res.send(model.xml, {'content-type': 'text/xml'});
             });
         },
@@ -93,7 +99,7 @@ server.prototype.projectDebug = function(req, res, next) {
     model.fetch({
         success: function(model, resp) {
             model.localize(model.toJSON(), function(err) {
-                if (err) return next(err);
+                if (err) return next({"message":err.message});
                 res.send({
                     debug: model.debug,
                     mml: model.mml,
@@ -126,3 +132,47 @@ server.prototype.getKey = function(req, res, next) {
     return next(new Error.HTTP(404));
 };
 
+server.prototype.updatesVersion = function(req, res, next) {
+    var settings = Bones.plugin.config;
+
+    // Config values to save.
+    var attr = {};
+
+    // Skip latest TileMill version check if disabled or
+    // we've checked the npm repo in the past 24 hours.
+    var skip = !settings.updates || (settings.updatesTime > Date.now() - 864e5);
+    var npm = require('npm');
+    Step(function() {
+        if (skip) return this();
+
+        console.warn('Checking for new version of TileMill...');
+        var opts = settings.httpProxy ? {proxy: settings.httpProxy} : {};
+        npm.load(opts, this);
+    }, function(err) {
+        if (skip || err) return this(err);
+
+        npm.localPrefix = path.join(process.env.HOME, '.tilemill');
+        npm.commands.view(['tilemill'], true, this);
+    }, function(err, resp) {
+        if (skip || err) return this(err);
+
+        if (!_(resp).size()) throw new Error('Latest TileMill package not found.');
+        if (!_(resp).toArray()[0].version) throw new Error('No version for TileMill package.');
+        console.warn('Latest version of TileMill is %s.', _(resp).toArray()[0].version);
+        attr.updatesVersion = _(resp).toArray()[0].version;
+        attr.updatesTime = Date.now();
+        this();
+    }, function(err) {
+        // Continue despite errors but log them to the console.
+        if (err) console.error(err);
+        // Save any config attributes.
+        if (_(attr).keys().length) (new models.Config).save(attr, {
+            error: function(m, err) { console.error(err); }
+        });
+        // Send the current updates settings and version
+        res.send({
+            updates: settings.updates,
+            updatesVersion: attr.updatesVersion || settings.updatesVersion
+        });
+    });
+}
diff --git a/servers/Core.bones b/servers/Core.bones
index 42fb47e..e159eb2 100644
--- a/servers/Core.bones
+++ b/servers/Core.bones
@@ -18,4 +18,5 @@ servers['Core'].prototype.initialize = function(app) {
     if (env === 'development') this.use(new servers['Debug'](app));
     this.use(new servers['Route'](app));
     this.use(new servers['Asset'](app));
+    this.enable('jsonp callback');
 };
diff --git a/servers/OAuth.bones b/servers/OAuth.bones
index 15bb2af..74ba884 100644
--- a/servers/OAuth.bones
+++ b/servers/OAuth.bones
@@ -5,13 +5,20 @@ var OAuth2Strategy = require('passport-oauth').OAuth2Strategy;
 
 server = Bones.Server.extend({});
 server.prototype.initialize = function(app, core) {
+    var back = function(req, res, next) {
+        if (req.query.error == 'access_denied') {
+            res.redirect('/#/settings');
+        } else {
+            next();
+        }
+    }
     var auth = passport.authenticate('mapbox', {
         session: false,
         failureRedirect: '/oauth/mapbox/fail'
     });
     this.use(passport.initialize());
     this.get('/oauth/mapbox', auth);
-    this.get('/oauth/mapbox/token', auth, function(req, res) {
+    this.get('/oauth/mapbox/token', back, auth, function(req, res) {
         // The user ID is *required* here. If it is not provided
         // (see error "handling" or lack thereof in Strategy#userProfile
         // below) we basically treat it as an error condition.
@@ -19,8 +26,8 @@ server.prototype.initialize = function(app, core) {
             syncAccount: req.user.id ? req.user.id : '',
             syncAccessToken: req.user.id ? req.user.accessToken : ''
         }, {
-            success: function() { res.redirect('/'); },
-            error: function() { res.redirect('/'); }
+            success: function() { res.redirect('/#/oauth/success'); },
+            error: function() { res.redirect('/#/oauth/error'); }
         });
     });
     this.get('/oauth/mapbox/fail', function(req, res) {
@@ -28,8 +35,8 @@ server.prototype.initialize = function(app, core) {
             syncAccount: '',
             syncAccessToken: ''
         }, {
-            success: function() { res.redirect('/'); },
-            error: function() { res.redirect('/'); }
+            success: function() { res.redirect('/#/oauth/error'); },
+            error: function() { res.redirect('/#/oauth/error'); }
         });
     });
 
@@ -41,37 +48,40 @@ server.prototype.initialize = function(app, core) {
         res.redirect('/');
     });
 
-    passport.use(new Strategy());
+    passport.use(this.strategy());
     passport.serializeUser(function(obj, done) { done(null, obj); });
     passport.deserializeUser(function(obj, done) { done(null, obj); });
 };
 
-// Add passport OAuth2 authorization.
-function Strategy() {
-    OAuth2Strategy.call(this, {
-        authorizationURL: config.syncURL + '/oauth/authorize',
-        tokenURL:         config.syncURL + '/oauth/access_token',
-        clientID:         'tilemill',
-        clientSecret:     'tilemill',
-        callbackURL:      'http://' + config.coreUrl + '/oauth/mapbox/token'
-    },
-    function(accessToken, refreshToken, profile, callback) {
-        profile.accessToken = accessToken;
-        profile.refreshToken = refreshToken;
-        return callback(null, profile);
-    });
-    this.name = 'mapbox';
-}
-util.inherits(Strategy, OAuth2Strategy);
-Strategy.prototype.userProfile = function(accessToken, done) {
-    this._oauth2.get(config.syncURL + '/oauth/user', accessToken, function (err, body) {
-        // oauth2 lib seems to not handle errors in a way where
-        // we can catch and handle them effectively. We attach them
-        // to the profile object here for our own custom handling.
-        if (err) {
-            return done(null, { error:err });
-        } else {
-            return done(null, JSON.parse(body));
-        }
-    });
+server.prototype.strategy = function() {
+    // Add passport OAuth2 authorization.
+    function Strategy() {
+        OAuth2Strategy.call(this, {
+            authorizationURL: config.syncURL + '/oauth/authorize',
+            tokenURL:         config.syncURL + '/oauth/access_token',
+            clientID:         'tilemill',
+            clientSecret:     'tilemill',
+            callbackURL:      'http://' + config.coreUrl + '/oauth/mapbox/token'
+        },
+        function(accessToken, refreshToken, profile, callback) {
+            profile.accessToken = accessToken;
+            profile.refreshToken = refreshToken;
+            return callback(null, profile);
+        });
+        this.name = 'mapbox';
+    }
+    util.inherits(Strategy, OAuth2Strategy);
+    Strategy.prototype.userProfile = function(accessToken, done) {
+        this._oauth2.get(config.syncURL + '/oauth/user', accessToken, function (err, body) {
+            // oauth2 lib seems to not handle errors in a way where
+            // we can catch and handle them effectively. We attach them
+            // to the profile object here for our own custom handling.
+            if (err) {
+                return done(null, { error:err });
+            } else {
+                return done(null, JSON.parse(body));
+            }
+        });
+    };
+    return new Strategy();
 };
diff --git a/servers/Tile.bones b/servers/Tile.bones
index a5cc03e..25f3ae4 100644
--- a/servers/Tile.bones
+++ b/servers/Tile.bones
@@ -18,13 +18,14 @@ server.prototype.start = function(callback) {
 };
 
 server.prototype.initialize = function() {
-    _.bindAll(this, 'thumb', 'load', 'mbtiles');
+    _.bindAll(this, 'thumb', 'projectStatus', 'load', 'mbtiles');
     this.port = settings.tilePort || this.port;
     this.enable('jsonp callback');
     this.use(this.cors);
     this.all('/tile/:id.mbtiles/:z/:x/:y.:format(png|grid.json)', this.mbtiles);
     this.all('/tile/:id/:z/:x/:y.:format(png|grid.json)', this.load);
     this.all('/tile/:id/thumb.png', this.thumb);
+    this.get('/tile/:id/project-status', this.projectStatus);
     this.all('/datasource/:id', this.datasource);
     this.get('/status', this.status);
     this.post('/restart', this.restart);
@@ -35,6 +36,13 @@ server.prototype.initialize = function() {
     });
 };
 
+server.prototype.projectStatus = function(req, res, next) {
+    var model = new models.Project({
+        id: req.param('id')
+    });
+    model.sync('status', model, res.send.bind(res), next);
+};
+
 server.prototype.load = function(req, res, next) {
     // This is the cache key in tilelive-mapnik, so make sure it
     // contains the mtime with _updated.
@@ -46,7 +54,8 @@ server.prototype.load = function(req, res, next) {
         pathname: path.join(settings.files, 'project', id, id + '.xml'),
         query: {
             updated:req.query.updated,
-            bufferSize:settings.bufferSize
+            scale: req.project && req.project.attributes.scale,
+            metatile: req.project && req.project.attributes.metatile
         },
         // Need not be set for a cache hit. Once the cache is
         // warmed the project need not be loaded/localized again.
diff --git a/templates/Autostyle._ b/templates/Autostyle._
index e664f88..f6935d5 100644
--- a/templates/Autostyle._
+++ b/templates/Autostyle._
@@ -8,7 +8,7 @@
 }<% } %><% if (get('geometry') == 'point' || get('geometry') == 'multipoint') { %>
 
 #<%= get('id') %> {
-  marker-width:3;
+  marker-width:6;
   marker-fill:#f45;
   marker-line-color:#813;
   marker-allow-overlap:true;
diff --git a/templates/Config._ b/templates/Config._
index 8410904..09d2a0f 100644
--- a/templates/Config._
+++ b/templates/Config._
@@ -11,13 +11,6 @@
     <h2>Application settings</h2>
   </li>
   <li>
-    <label for='bufferSize'>Render buffer</label>
-    <div class='slider' data-key='bufferSize' data-step='16' data-min='0' data-max='1024'></div>
-    <div class='description'>
-      Mapnik render buffer in pixels. Increase this value if labels appear cut off at tile edges.
-    </div>
-  </li>
-  <li>
     <label for='files'>Documents</label>
     <input type='text' name='files' size='40' value='<%= get('files') %>' />
     <div class='description'>
@@ -26,18 +19,18 @@
   </li>
   <li>
     <label for='syncAccount'>MapBox</label>
-    <div class='syncOn <%= get('syncAccount') ? '' : 'dependent' %>'>
+    <div class='syncOn <%= get('syncAccount') && get('syncAccessToken') ? '' : 'dependent' %>'>
       <span style='margin-right:10px'>
       <a target='_blank' href='<%=get('syncURL')%>/<%=get('syncAccount')%>'><%=get('syncURL')%>/<%=get('syncAccount')%></a>
       </span>
       <a class='button' href='/oauth/mapbox'>Reauthorize</a>
       <a class='button' href='#disable'>Disable</a>
     </div>
-    <div class='syncOff <%= get('syncAccount') ? 'dependent' : '' %>'>
+    <div class='syncOff <%= get('syncAccount') && get('syncAccessToken') ? 'dependent' : '' %>'>
       <a class='button' href='/oauth/mapbox'>Authorize</a>
     </div>
     <div class='description'>
-      <% if (get('syncAccount')) { %>
+      <% if (get('syncAccount') && get('syncAccessToken')) { %>
       Upload maps to this account.
       <% } else { %>
       Authorize TileMill to upload to your MapBox account.
@@ -45,12 +38,24 @@
     </div>
   </li>
   <li>
+    <label for='httpProxy'>HTTP Proxy</label>
+    <input type='text' name='httpProxy' size='40' placeholder='http://user:pass@hostname:port' value='<%= get('httpProxy') %>' />
+    <div class='description'>
+      HTTP proxy to be used when connecting to the Internet.
+    </div>
+  </li>
+  <li>
     <label for='profile'>System profile</label>
     <input type='checkbox' name='profile' <% if (get('profile')) %>checked='checked'<% ; %> /> Report system profile anonymously.
     <div class='description'>
       Help the TileMill development team by reporting your system profile:<br/>
+      <% if(window.abilities.platform !== 'darwin'){ %>
       <%= window.abilities.cpus.length + ' x ' + window.abilities.cpus[0].model %><br/>
       <%= (window.abilities.totalmem / 1e9).toFixed(1) %>GB RAM /
+      <% } else { %>
+      <%= window.abilities.cpus[0].model + ' / ' + window.abilities.cpus.length + '×' + (window.abilities.cpus[0].speed/1000).toFixed(1) %>GHz<br/>
+      <%= (window.abilities.totalmem / 1073741824).toFixed(1) %>GB RAM /
+      <% } %>
       <%= window.abilities.platform %> /
       TileMill <%= window.abilities.tilemill.version %>
     </div>
@@ -64,6 +69,13 @@
     <% } %>
   </li>
   <% } %>
+  <li>
+    <label for='verbose'>Logging</label>
+    <input type='checkbox' name='verbose' <% if (get('verbose') == 'on') %>checked='checked'<% ; %> /> Use verbose logging
+    <div class='description'>
+      Check this to see more output in your logs about what TileMill processes are doing
+    </div>
+  </li>
   <li class='buttons'>
     <input type='submit' value='Saved' class='disabled' />
   </li>
diff --git a/templates/Layer._ b/templates/Layer._
index 059fe0d..f350414 100644
--- a/templates/Layer._
+++ b/templates/Layer._
@@ -11,7 +11,7 @@ var advancedOptions = function(obj) {
         'type', 'file',
         'table', 'host', 'port', 'user', 'password', 'dbname',
         'extent', 'key_field', 'geometry_field', 'type', 'attachdb',
-        'srs', 'id', 'project'
+        'srs', 'id', 'project', 'extent_cache'
     ];
     var options = [];
     return _(obj).chain()
@@ -119,11 +119,6 @@ var connectionOptions = function(obj) {
       </div>
     </li>
     <li>
-      <label for='extent'>Extent</label>
-      <input type='text' name='Datasource.extent' value='<%=Datasource.extent%>' />
-      <span class='description'>limit the query by this bounding box</span>
-    </li>
-    <li>
       <label for='extent'>Unique key field</label>
       <input type='text' name='Datasource.key_field' value='<%=Datasource.key_field%>' />
       <span class='description'>SQL field containing a unique key for each feature</span>
@@ -133,6 +128,32 @@ var connectionOptions = function(obj) {
       <input type='text' name='Datasource.geometry_field' value='<%=Datasource.geometry_field%>' />
       <span class='description'>SQL field containing feature geometry</span>
     </li>
+    <li>
+      <label for='extent'>Extent</label>
+      <select name='Datasource.extent_cache'>
+        <option value='auto' <% if (Datasource.extent_cache === 'auto' || !Datasource.extent_cache) { %>selected='selected'<% } %>>
+          Pre-calculate
+        </option>
+        <option value='dynamic' <% if (Datasource.extent_cache === 'dynamic') { %>selected='selected'<% } %>>
+          Dynamic
+        </option>
+        <option value='custom' <% if (Datasource.extent_cache === 'custom') { %>selected='selected'<% } %>>
+          Custom
+        </option>
+      </select>
+      <input type='text' name='Datasource.extent' <% if (Datasource.extent_cache === 'auto' || !Datasource.extent_cache ) { %>style="display:none"<% } %>
+          value='<%=Datasource.extent%>' size='40' placeholder='' />
+      <a href='#extentCacheFlush' class='button open' <% if (Datasource.extent_cache === 'custom') { %>style="display:none"<% } %>'>Clear cache</a>
+      <small for='auto' class='description'<% if (Datasource.extent_cache !== 'auto' && Datasource.extent_cache) { %>style="display:none"<% }%>>
+          Auto-calculate and cache the extent to limit the query by.
+      </small>
+      <small for='dynamic'class='description' <% if (Datasource.extent_cache !== 'dynamic') { %>style="display:none"<% } %>>
+          Dynamically calculate extent for each map save (can be slow).
+      </small>
+      <small for='custom' class='description' <% if (Datasource.extent_cache !== 'custom') { %>style="display:none"<% } %>>
+          Limit the query with a custom 'minx,miny,maxx,maxy' in the projection of the data.
+      </small>
+    </li>
     <% } %>
 
     <li>
diff --git a/plugins/map/templates/Map._ b/templates/Map._
similarity index 100%
rename from plugins/map/templates/Map._
rename to templates/Map._
diff --git a/templates/Metadata._ b/templates/Metadata._
index 52ffcfa..17c1df3 100644
--- a/templates/Metadata._
+++ b/templates/Metadata._
@@ -8,9 +8,7 @@ var get = _(project.get).bind(project);
 <form><ul class='form fill-e scrolling'>
   <li class='text'>
     <h2><%= title %></h2>
-
     <% if (model.get('format') === 'sync') { %>
-    <% if (config.get('syncAccount') && config.get('syncAccessToken')) { %>
     <% var accountURL = config.get('syncURL') + '/' + config.get('syncAccount'); %>
     <% var mapURL = config.get('syncURL') + '/' + config.get('syncAccount') + '/map/' + model.get('project'); %>
       <div class='syncHelp'>
@@ -18,18 +16,8 @@ var get = _(project.get).bind(project);
         <b><a href='<%= mapURL %>' target='_blank'><%=mapURL%></a></b>
       </div>
       <div class='buttons centered'>
-        <a class='button' href='#/settings'>Use another account</a>
-        <a class='button' href='<%= accountURL %>' target='_blank'>My account</a>
-    </div>
-    <% } else { %>
-      <div class='syncHelp'>
-        If you don't have a MapBox account, we'll host your map free for 7 days.
+        <a class='button' href='#/settings'>Change account</a>
       </div>
-      <div class='buttons centered'>
-        <a class='button' href='#/settings'>Use my account</a>
-        <a class='button' href='http://mapbox.com/tour/' target='_blank'>Learn more</a>
-      </div>
-    <% } %>
     <% } %>
   </li>
   <% if (type === 'tiles') { %>
@@ -99,6 +87,20 @@ var get = _(project.get).bind(project);
     <input type='text' name='bounds' size='20' value='<%=get('bounds').join(',')%>' />
     <small class='description'>Shift + drag to select bounds.</small>
   </li>
+  <% if (model === project) { %>
+  <li>
+    <label for='scale'>Scale factor</label>
+    <div class='slider' data-key='scale' data-step='0.1' data-min='1' data-max='5'></div>
+    <small class='description'>Factor to scale map by.</small>
+  </li>
+  <% } %>
+  <% if (type === 'tiles') { %>
+  <li>
+    <label for='metatile'>MetaTile size</label>
+    <div class='slider' data-key='metatile' data-step='1' data-min='1' data-max='15'></div>
+    <small class='description'></small>
+  </li>
+  <% } %>
   <% if (model !== project && type === 'tiles') { %>
   <li>
     <input type='checkbox' name='_saveProject' value='1' <% if (model.get('format') === 'sync') %>checked='checked' disabled='disabled'<%;%>>
diff --git a/templates/MetadataSignup._ b/templates/MetadataSignup._
new file mode 100644
index 0000000..c5a6c71
--- /dev/null
+++ b/templates/MetadataSignup._
@@ -0,0 +1,18 @@
+<% var get = _(project.get).bind(project); %>
+<div class='fill-w' id='meta-map'>
+  <div class='zoom-display'>Zoom <span class='zoom'></span></div>
+</div>
+
+<form><ul class='form fill-e scrolling'>
+  <li class='text'>
+    <h2><%= title %></h2>
+
+    <div class='syncHelp'>
+      Sign in to your MapBox account to upload your map to the web.
+    </div>
+  </li>
+  <li class='buttons'>
+    <a class='button' href='/oauth/mapbox'>Next</a>
+    <input type='button' value='Cancel' class='cancel' />
+  </li>
+</ul></form>
diff --git a/templates/Plugin._ b/templates/Plugin._
index b568c83..dd373c7 100644
--- a/templates/Plugin._
+++ b/templates/Plugin._
@@ -1,6 +1,7 @@
 <%
 var installed = !!window.abilities.plugins[id];
 var compatible = semver.satisfies(window.abilities.tilemill.version, get('engines').tilemill);
+var upgradable = installed && get('latest') && semver.gt(get('latest'), get('version'));
 %>
 <li><div class='plugin <%= installed ? 'raised' : 'sunken' %>'>
   <%= escape('name') %>
@@ -11,11 +12,16 @@ var compatible = semver.satisfies(window.abilities.tilemill.version, get('engine
     <% if (!compatible) %>
     <span class='error'>Incompatible with TileMill <%=window.abilities.tilemill.version%></span>
     <%;%>
+    <% if (get('broken')) %>
+    <span class='error'>Broken, uninstalling is highly recommended</span>
+    <%;%>
+
   </small>
   <div class='actions'>
     <% if (get('core')) { %>
     <span class='badge'>Core</span>
     <% } else { %>
+      <% if (upgradable) %><a class='button upgrade' href='#<%=id%>'>Upgrade</a><% ; %>
       <% if (installed) %><a class='button uninstall' href='#<%=id%>'>Uninstall</a><% ; %>
       <% if (!installed && compatible) %><a class='button install' href='#<%=id%>'>Install</a><% ; %>
     <% } %>
diff --git a/templates/Plugins._ b/templates/Plugins._
index 7b17b3d..6da0a4c 100644
--- a/templates/Plugins._
+++ b/templates/Plugins._
@@ -17,10 +17,12 @@
   </li>
   <li class='plugins'><ul class='grid clearfix'>
   <% var show = available.chain()
-    .filter(function(m) { return !m.get('installed') })
+    .filter(function(m) { return !window.abilities.plugins[m.id] })
     .map(templates.Plugin)
-    .value(); %>
-  <%= show.join('') %>
+    .value();
+    show = show.length ? show.join('') : available.length ? '<div class="empty description">No plugins found.</div>' : '';
+    %>
+  <%= show %>
   </ul></li>
   <div class='mask'></div>
 </ul>
diff --git a/templates/Project._ b/templates/Project._
index 3b23ee4..651b8c2 100644
--- a/templates/Project._
+++ b/templates/Project._
@@ -3,7 +3,10 @@
 
   <div class='workspace fill-e'>
     <div class='header'>
-    <h1 class='name'><%= get('name') || get('id') %></h1>
+    <div>
+       <span class='name'><%= get('name') || get('id') %></span>
+       <span class='project-status' title="check the logs for more details"></span>
+    </div>
     <div class='actions joined'>
       <a class='button disabled' href='#save'><span class='icon reverse edit labeled'></span> Save</a>
       <span class='button dropdown' href='#export'>
diff --git a/templates/ProjectLayer._ b/templates/ProjectLayer._
index 7b495b6..0624932 100644
--- a/templates/ProjectLayer._
+++ b/templates/ProjectLayer._
@@ -1,4 +1,4 @@
-<li>
+<li<% if (get('status') === 'off') { %> class='status-off'<% } %>>
   <span class='handle fill-w'>
     <a title='<%= get('geometry') %>' class='icon geometry geometry-<%= get('geometry') %>'></a>
   </span>
@@ -7,6 +7,8 @@
     <% if (get('geometry') !== 'raster') { %>
       <a title='Features (<%= id %>)' class='icon inspect drawer' href='#<%= id %>'>Features (<%= id %>)</a>
     <% } %>
+    <a title='Zoom to extent (<%= id %>)' class='icon extent' href='#<%= id%>'>Zoom to <%= id %></a>
+    <a title='Toggle visibility of <%= id %>' class='icon visibility' href='#<%= id %>'>Hide <%= id %></a>
     <a title='Edit <%= id %>' class='icon edit popup' href='#<%= id %>'>Edit <%= id %></a>
     <a title='Delete <%= id %>' class='icon delete' href='#<%= id %>'>Delete <%= id %></a>
   </span>
diff --git a/test/abilities.test.js b/test/abilities.test.js
index 8cd5391..1651df3 100644
--- a/test/abilities.test.js
+++ b/test/abilities.test.js
@@ -1,39 +1,58 @@
 var assert = require('assert');
+var core;
+var tile;
 
-require('./support/start')(function(command) {
-    command.servers['Tile'].close();
-
-    exports['test abilities endpoint'] = function() {
-        assert.response(command.servers['Core'],
-            { url: '/assets/tilemill/js/abilities.js' },
-            { status: 200 },
-            function(res) {
-                var body = res.body.replace(/^\s*var\s+abilities\s*=\s*(.+?);?$/, '$1');
-                var abilities = JSON.parse(body);
-
-                assert.ok(/v\d+.\d+.\d+-\d+-[a-z0-9]+/.test(abilities.version[0]));
-                assert.ok(/\d+.\d+.\d+.\d+/.test(abilities.version[1]));
-                assert.ok(abilities.fonts.indexOf('Arial Regular') >= 0 ||
-                          abilities.fonts.indexOf('DejaVu Sans Book') >= 0);
-                assert.deepEqual([0,206,209], abilities.carto.colors.darkturquoise);
-                assert.deepEqual([
-                    "background-color",
-                    "background-image",
-                    "srs",
-                    "buffer-size",
-                    "base",
-                    "paths-from-xml",
-                    "minimum-version",
-                    "font-directory"
-                ], Object.keys(abilities.carto.symbolizers.map));
-
-                assert.deepEqual({
-                    mbtiles: true,
-                    png: true,
-                    pdf: true,
-                    svg: true
-                }, abilities.exports);
-            }
-        );
-    };
+describe('abilities', function() {
+
+before(function(done) {
+    require('./support/start').start(function(command) {
+        core = command.servers['Core'];
+        tile = command.servers['Tile'];
+        done();
+    });
+});
+
+after(function(done) {
+    core.close();
+    tile.close();
+    done();
+});
+
+it('GET should return JSON', function(done) {
+    assert.response(core,
+        { url: '/assets/tilemill/js/abilities.js' },
+        { status: 200 },
+        function(res) {
+            var body = res.body.replace(/^\s*var\s+abilities\s*=\s*(.+?);?$/, '$1');
+            var abilities = JSON.parse(body);
+
+            assert.ok(/v\d+.\d+.\d+-\d+-[a-z0-9]+/.test(abilities.version[0]));
+            assert.ok(/\d+.\d+.\d+.\d+/.test(abilities.version[1]));
+            assert.ok(abilities.fonts.indexOf('Arial Regular') >= 0 ||
+                      abilities.fonts.indexOf('DejaVu Sans Book') >= 0);
+            assert.deepEqual([0,206,209], abilities.carto.colors.darkturquoise);
+            assert.deepEqual([
+                "background-color",
+                "background-image",
+                "srs",
+                "buffer-size",
+                "maximum-extent",
+                "base",
+                "paths-from-xml",
+                "minimum-version",
+                "font-directory"
+            ], Object.keys(abilities.carto.symbolizers.map));
+
+            assert.deepEqual({
+                mbtiles: true,
+                png: true,
+                pdf: true,
+                svg: true
+            }, abilities.exports);
+            done();
+        }
+    );
 });
+
+});
+
diff --git a/test/config.test.js b/test/config.test.js
index 0bc51c4..d0a4aab 100644
--- a/test/config.test.js
+++ b/test/config.test.js
@@ -1,91 +1,129 @@
+var _ = require('underscore');
 var assert = require('assert');
 var Step = require('step');
+var core;
+var tile;
 
-require('./support/start')(function(command) {
-command.servers['Tile'].close();
+// Reduce config response body down to attributes we care about.
+var attr = function(body) {
+    var data = JSON.parse(body);
+    return _(data).reduce(function(memo, val, key) {
+        if ('files examples host port sampledata listenHost'.indexOf(key) >= 0) {
+            memo[key] = val;
+        }
+        return memo;
+    }, {});
+};
 
-exports['config'] = function() {
+describe('config', function() {
 
-var server = command.servers['Core'];
-
-// GET config
-Step(function() {
-    assert.response(server, {
-        url: '/api/Config/config'
-    }, { status:200 }, this);
-}, function(res) {
-    assert.deepEqual(JSON.parse(res.body), {
-        'files': '~',
-        'examples': false,
-        'host': [],
-        'port': 20009,
-        'sampledata': true,
-        'listenHost': '127.0.0.1',
-        'bufferSize': 128
+before(function(done) {
+    require('./support/start').start(function(command) {
+        core = command.servers['Core'];
+        tile = command.servers['Tile'];
+        done();
     });
-    this();
-// PUT config
-}, function() {
-    assert.response(server, {
-        url: '/api/Config/config',
-        method: 'PUT',
-        headers: {
-            'content-type': 'application/json',
-            'cookie': 'bones.token=asdf'
+});
+
+after(function(done) {
+    core.close();
+    tile.close();
+    done();
+});
+
+it('GET should return JSON', function(done) {
+    assert.response(core,
+        { url: '/api/Config/config' },
+        { status:200 },
+        function(res) {
+            assert.deepEqual(attr(res.body), {
+                'files': '~',
+                'examples': false,
+                'host': [],
+                'port': 20009,
+                'sampledata': true,
+                'listenHost': '127.0.0.1'
+            });
+            done();
+        }
+    );
+});
+it('PUT should update config', function(done) {
+    assert.response(core,
+        {
+            url: '/api/Config/config',
+            method: 'PUT',
+            headers: {
+                'content-type': 'application/json',
+                'cookie': 'bones.token=asdf'
+            },
+            data: JSON.stringify({
+                'port': 20010,
+                'bones.token': 'asdf'
+            })
         },
-        data: JSON.stringify({
-            'bufferSize': 1024,
-            'bones.token': 'asdf'
-        })
-    }, { status:200 }, this);
-}, function(res) {
-    assert.response(server, {
-        url: '/api/Config/config',
-        method: 'GET'
-    }, { status:200 }, this);
-// Confirm PUT
-}, function(res) {
-    assert.deepEqual(JSON.parse(res.body), {
-        'files': '~',
-        'bufferSize': 1024,
-        'examples': false,
-        'host': [],
-        'port': 20009,
-        'sampledata': true,
-        'listenHost': '127.0.0.1'
-    });
-    this();
-// PUT invalid config
-}, function() {
-    assert.response(server, {
-        url: '/api/Config/config',
-        method: 'PUT',
-        headers: {
-            'content-type': 'application/json',
-            'cookie': 'bones.token=asdf'
+        { status:200 },
+        function(res) {
+            assert.deepEqual(attr(res.body), {});
+            done();
+        }
+    );
+});
+it('GET should return updated config', function(done) {
+    assert.response(core,
+        { url: '/api/Config/config' },
+        { status:200 },
+        function(res) {
+            assert.deepEqual(attr(res.body), {
+                'files': '~',
+                'examples': false,
+                'host': [],
+                'port': 20009,
+                'sampledata': true,
+                'listenHost': '127.0.0.1'
+            });
+            done();
+        }
+    );
+});
+it('PUT should 409 on invalid config', function(done) {
+    assert.response(core,
+        {
+            url: '/api/Config/config',
+            method: 'PUT',
+            headers: {
+                'content-type': 'application/json',
+                'cookie': 'bones.token=asdf'
+            },
+            data: JSON.stringify({
+                'port': 'asdf',
+                'bones.token': 'asdf'
+            })
         },
-        data: JSON.stringify({
-            'bufferSize': 'asdf',
-            'bones.token': 'asdf'
-        })
-    }, { status:409 }, this);
-}, function(res) {
-    assert.equal(res.body, 'Error: Instance is not a required type (bufferSize)');
-    this();
-// DELETE config
-}, function() {
-    assert.response(server, {
-        url: '/api/Config/config',
-        method: 'DELETE',
-        headers: {
-            'content-type': 'application/json',
-            'cookie': 'bones.token=asdf'
+        { status:409 },
+        function(res) {
+            assert.equal(res.body, 'Instance is not a required type (port)');
+            done();
+        }
+    );
+});
+it('DELETE should 409', function(done) {
+    assert.response(core,
+        {
+            url: '/api/Config/config',
+            method: 'DELETE',
+            headers: {
+                'content-type': 'application/json',
+                'cookie': 'bones.token=asdf'
+            },
+            data: JSON.stringify({ 'bones.token': 'asdf' })
         },
-        data: JSON.stringify({ 'bones.token': 'asdf' })
-    }, { status:409 }, this);
-}, function(res) {
-    assert.equal(res.body, 'Error: Method not supported.');
+        { status:409 },
+        function(res) {
+            assert.equal(res.body, 'Method not supported.');
+            done();
+        }
+    );
 });
 
-}});
-
+});
diff --git a/test/datasource.test.js b/test/datasource.test.js
index 365588a..23be766 100644
--- a/test/datasource.test.js
+++ b/test/datasource.test.js
@@ -1,73 +1,107 @@
-var assert = require('assert'),
-    fs = require('fs');
+var assert = require('assert');
+var fs = require('fs');
 var path = require('path');
+var diff = require('difflet')({ indent : 2 });
+var core;
+var tile;
 
 function readJSON(name) {
     var json = fs.readFileSync(path.resolve(__dirname + '/fixtures/' + name + '.json'), 'utf8');
     return JSON.parse(json);
 }
 
-require('./support/start')(function(command) {
-    command.servers['Core'].close();
+describe('datasource', function() {
 
-    exports['test sqlite datasource'] = function() {
-        assert.response(command.servers['Tile'],
-            { url: '/datasource/world?file=' + encodeURIComponent(__dirname + '/fixtures/countries.sqlite') + '&table=countries&id=world&type=sqlite&project=demo_01&srs=null' },
-            { status: 200 },
-            function(res) {
-                var body = JSON.parse(res.body), datasource = readJSON('datasource-sqlite');
-                datasource.url = __dirname + '/fixtures/countries.sqlite';
-                assert.deepEqual(datasource, body);
-            }
-        );
-    };
+before(function(done) {
+    require('./support/start').startPostgis(function(command) {
+        core = command.servers['Core'];
+        tile = command.servers['Tile'];
+        done();
+    });
+});
+
+after(function(done) {
+    core.close();
+    tile.close();
+    done();
+});
+
+it('GET sqlite', function(done) {
+    assert.response(tile,
+        { url: '/datasource/world?file=' + encodeURIComponent(__dirname + '/fixtures/countries.sqlite') + '&table=countries&id=world&type=sqlite&project=demo_01&srs=%2Bproj%3Dmerc+%2Ba%3D6378137+%2Bb%3D6378137+%2Blat_ts%3D0.0+%2Blon_0%3D0.0+%2Bx_0%3D0.0+%2By_0%3D0+%2Bk%3D1.0+%2Bunits%3Dm+%2Bnadgrids%3D%40null+%2Bwktext+%2Bno_defs+%2Bover' },
+        { status: 200 },
+        function(res) {
+            var body = JSON.parse(res.body), datasource = readJSON('datasource-sqlite');
+            datasource.url = __dirname + '/fixtures/countries.sqlite';
+            assert.deepEqual(datasource, body, diff.compare(datasource, body));
+            done();
+        }
+    );
+});
 
-    /* disable back to back shapefile tests to avoid crash
-       until the root cause is clear: https://github.com/mapbox/tilemill/issues/1006
-    */
-    
-    /*exports['test shapefile datasource'] = function() {
-        assert.response(command.servers['Core'],
-            { url: '/api/Datasource/world?file=http%3A%2F%2Ftilemill-data.s3.amazonaws.com%2Fworld_borders_merc.zip&type=shape&id=world&project=demo_01' },
-            { status: 200 },
-            function(res) {
-                var body = JSON.parse(res.body);
-                assert.deepEqual(readJSON('datasource-shp'), body);
-            }
-        );
-    };
-    */
+it('GET shapefile datasource', function(done) {
+    assert.response(tile,
+        { url: '/datasource/world?file=http%3A%2F%2Ftilemill-data.s3.amazonaws.com%2Fworld_borders_merc.zip&type=shape&id=world&project=demo_01' },
+        { status: 200 },
+        function(res) {
+            var body = JSON.parse(res.body);
+            var expected = readJSON('datasource-shp');
+            assert.deepEqual(expected.fields, body.fields);
+            assert.equal(expected.geometry_type, body.geometry_type);
+            assert.equal(expected.id, body.id);
+            assert.equal(expected.project, body.project);
+            assert.equal(expected.type, body.type);
+            assert.equal(expected.url, body.url);
+            assert.deepEqual(expected.features, body.features);
+            done();
+        }
+    );
+});
 
-    exports['test shapefile datasource with features'] = function() {
-        assert.response(command.servers['Tile'],
-            { url: '/datasource/world?file=http%3A%2F%2Ftilemill-data.s3.amazonaws.com%2Fworld_borders_merc.zip&type=shape&id=world&project=demo_01&features=true' },
-            { status: 200 },
-            function(res) {
-                var body = JSON.parse(res.body);
-                assert.deepEqual(readJSON('datasource-shp-features'), body);
-            }
-        );
-    };
+it('GET shapefile datasource with features', function(done) {
+    assert.response(tile,
+        { url: '/datasource/world?file=http%3A%2F%2Ftilemill-data.s3.amazonaws.com%2Fworld_borders_merc.zip&type=shape&id=world&project=demo_01&features=true' },
+        { status: 200 },
+        function(res) {
+            var body = JSON.parse(res.body);
+            var expected = readJSON('datasource-shp-features');
+            assert.deepEqual(expected.fields, body.fields);
+            assert.equal(expected.geometry_type, body.geometry_type);
+            assert.equal(expected.id, body.id);
+            assert.equal(expected.project, body.project);
+            assert.equal(expected.type, body.type);
+            assert.equal(expected.url, body.url);
+            assert.deepEqual(expected.features, body.features);
+            done();
+        }
+    );
+});
 
-    exports['test postgis datasource'] = function() {
-        assert.response(command.servers['Tile'],
-            { url: '/datasource/postgis?table%3D%2210m-admin-0-boundary-lines-land%22&key_field=&geometry_field=&extent=-15312095%2C-6980576.5%2C15693558%2C11093272&type=postgis&dbname=tilemill_test&id=postgis&srs=%2Bproj%3Dmerc+%2Ba%3D6378137+%2Bb%3D6378137+%2Blat_ts%3D0.0+%2Blon_0%3D0.0+%2Bx_0%3D0.0+%2By_0%3D0+%2Bk%3D1.0+%2Bunits%3Dm+%2Bnadgrids%3D%40null+%2Bwktext+%2Bno_defs+%2Bover&project=demo_01' },
-            { status: 200 },
-            function(res) {
-                var body = JSON.parse(res.body);
-                assert.deepEqual(readJSON('datasource-postgis'), body);
-            }
-        );
-    };
+it('GET postgis datasource', function(done) {
+    assert.response(tile,
+        { url: '/datasource/postgis?table%3D%2210m-admin-0-boundary-lines-land%22&key_field=&geometry_field=&extent=-15312095%2C-6980576.5%2C15693558%2C11093272&type=postgis&dbname=tilemill_test&id=postgis&srs=%2Bproj%3Dmerc+%2Ba%3D6378137+%2Bb%3D6378137+%2Blat_ts%3D0.0+%2Blon_0%3D0.0+%2Bx_0%3D0.0+%2By_0%3D0+%2Bk%3D1.0+%2Bunits%3Dm+%2Bnadgrids%3D%40null+%2Bwktext+%2Bno_defs+%2Bover&project=demo_01' },
+        { status: 200 },
+        function(res) {
+            var body = JSON.parse(res.body);
+            var datasource = readJSON('datasource-postgis');
+            assert.deepEqual(datasource, body, diff.compare(datasource, body));
+            done();
+        }
+    );
+});
+
+it('GET postgis datasource with features', function(done) {
+    assert.response(tile,
+        { url: '/datasource/postgis?table%3D%2210m-admin-0-boundary-lines-land%22&key_field=&geometry_field=&extent=-15312095%2C-6980576.5%2C15693558%2C11093272&type=postgis&dbname=tilemill_test&id=postgis&srs=%2Bproj%3Dmerc+%2Ba%3D6378137+%2Bb%3D6378137+%2Blat_ts%3D0.0+%2Blon_0%3D0.0+%2Bx_0%3D0.0+%2By_0%3D0+%2Bk%3D1.0+%2Bunits%3Dm+%2Bnadgrids%3D%40null+%2Bwktext+%2Bno_defs+%2Bover&project=demo_01&features=true' },
+        { status: 200 },
+        function(res) {
+            var body = JSON.parse(res.body);
+            var datasource = readJSON('datasource-postgis-features');
+            assert.deepEqual(datasource.fields, body.fields, diff.compare(datasource.fields, body.fields));
+            assert.deepEqual(datasource.features, body.features, diff.compare(datasource.features, body.features));
+            done();
+        }
+    );
+});
 
-    exports['test postgis datasource with features'] = function() {
-        assert.response(command.servers['Tile'],
-            { url: '/datasource/postgis?table%3D%2210m-admin-0-boundary-lines-land%22&key_field=&geometry_field=&extent=-15312095%2C-6980576.5%2C15693558%2C11093272&type=postgis&dbname=tilemill_test&id=postgis&srs=%2Bproj%3Dmerc+%2Ba%3D6378137+%2Bb%3D6378137+%2Blat_ts%3D0.0+%2Blon_0%3D0.0+%2Bx_0%3D0.0+%2By_0%3D0+%2Bk%3D1.0+%2Bunits%3Dm+%2Bnadgrids%3D%40null+%2Bwktext+%2Bno_defs+%2Bover&project=demo_01&features=true' },
-            { status: 200 },
-            function(res) {
-                var body = JSON.parse(res.body);
-                assert.deepEqual(readJSON('datasource-postgis-features'), body);
-            }
-        );
-    };
 });
diff --git a/test/duplicate_module.test.js b/test/duplicate_module.test.js
new file mode 100644
index 0000000..7cb1ba5
--- /dev/null
+++ b/test/duplicate_module.test.js
@@ -0,0 +1,25 @@
+var assert = require('assert');
+var exec = require('child_process').exec;
+
+var count_module = function(name,callback) {
+    var cmd = 'npm ls | grep ' + name;
+    exec(cmd,
+        function (error, stdout, stderr) {
+        //if (stderr) return callback(new Error(stderr));
+        return callback(null,stdout.match(/@/g).length);
+    });
+};
+
+describe('config loading pwnage', function() {
+
+['optimist','sqlite3'].forEach(function(mod) {
+    it('there should only be one ' + mod + ' module otherwise you are hosed', function(done) {
+         count_module(mod, function(err,count) {
+            if (err) throw err;
+            assert.equal(count,1,'you have more than one copy of ' + mod + ' (based on npm ls output)')
+            done();
+        });
+    });
+});
+
+});
\ No newline at end of file
diff --git a/test/export.test.js b/test/export.test.js
index 444411c..a68e16f 100644
--- a/test/export.test.js
+++ b/test/export.test.js
@@ -1,76 +1,91 @@
 var assert = require('assert');
 var fs = require('fs');
 var path = require('path');
+var core;
+var tile;
 
 function readJSON(name) {
     var json = fs.readFileSync(path.resolve(__dirname + '/fixtures/' + name + '.json'), 'utf8');
     return JSON.parse(json);
 }
 
-require('./support/start')(function(command) {
-    command.servers['Tile'].close();
+describe('export', function() {
 
-    exports['test export job creation'] = function(beforeExit) {
-        var completed = false;
-        var id = Date.now().toString();
-        var job = readJSON('export-job');
-        var token = job['bones.token'];
-        job.id = id;
-
-        assert.response(command.servers['Core'], {
-            url: '/api/Export/' + id,
-            method: 'PUT',
-            data: JSON.stringify(job),
-            headers: {
-                cookie: "bones.token=" + token,
-                'content-type': "application/json"
-            }
-        }, {
-            body: '{}',
-            status: 200
-        }, function (res) {
-            assert.response(command.servers['Core'], {
-                url: '/api/Export'
-            }, { status: 200 }, function(res) {
-                var body = JSON.parse(res.body);
-                job.status = "processing";
-                assert.ok(body[0].pid);
-                assert.ok(body[0].created);
-                delete job['bones.token'];
-                delete body[0].created;
-                delete body[0].pid;
-                assert.deepEqual(job, body[0]);
-
-                job['bones.token'] = token;
-                assert.response(command.servers['Core'], {
-                    url: '/api/Export/' + id,
-                    method: 'DELETE',
-                    headers: {
-                        cookie: "bones.token=" + token,
-                        'content-type': "application/json"
-                    },
-                    body: JSON.stringify(job)
-                }, {
-                    body: '{}',
-                    status: 200
-                }, function (res) {
-                    assert.response(command.servers['Core'], {
-                        url: '/api/Export'
-                    }, { status: 200 }, function(res) {
-                        completed = true;
-                        var body = JSON.parse(res.body);
-                        assert.deepEqual([], body);
-                    });
-                });
-            });
-        });
-
-        beforeExit(function() {
-            assert.ok(completed);
-        })
-    };
+before(function(done) {
+    require('./support/start').start(function(command) {
+        core = command.servers['Core'];
+        tile = command.servers['Tile'];
+        done();
+    });
 });
 
+after(function(done) {
+    core.close();
+    tile.close();
+    done();
+});
 
+var id = Date.now().toString();
+var job = readJSON('export-job');
+var token = job['bones.token'];
+job.id = id;
+it('PUT should create export job', function(done) {
+    assert.response(core, {
+        url: '/api/Export/' + id,
+        method: 'PUT',
+        data: JSON.stringify(job),
+        headers: {
+            cookie: "bones.token=" + token,
+            'content-type': "application/json"
+        }
+    }, {
+        body: '{}',
+        status: 200
+    }, function (res) {
+        assert.deepEqual(JSON.parse(res.body), {});
+        done();
+    });
+});
+it('GET should retrieve export job', function(done) {
+    assert.response(core, {
+        url: '/api/Export'
+    }, { status: 200 }, function(res) {
+        var body = JSON.parse(res.body);
+        job.status = "processing";
+        assert.ok(body[0].pid);
+        assert.ok(body[0].created);
+        delete job['bones.token'];
+        delete body[0].created;
+        delete body[0].pid;
+        assert.deepEqual(job, body[0]);
+        done();
+    });
+});
+it('DELETE should stop export job', function(done) {
+    job['bones.token'] = token;
+    assert.response(core, {
+        url: '/api/Export/' + id,
+        method: 'DELETE',
+        headers: {
+            cookie: "bones.token=" + token,
+            'content-type': "application/json"
+        },
+        body: JSON.stringify(job)
+    }, {
+        body: '{}',
+        status: 200
+    }, function (res) {
+        assert.deepEqual(JSON.parse(res.body), {});
+        done();
+    });
+});
+it('GET should find no export jobs', function(done) {
+    assert.response(core, {
+        url: '/api/Export'
+    }, { status: 200 }, function(res) {
+        assert.deepEqual([], JSON.parse(res.body));
+        done();
+    });
+});
 
-
+});
diff --git a/test/fixtures/created-project.json b/test/fixtures/created-project.json
index 70c2f59..549cd5c 100644
--- a/test/fixtures/created-project.json
+++ b/test/fixtures/created-project.json
@@ -22,6 +22,8 @@
     "format": "png",
     "grids": [ "http://localhost:20008/tile/demo_02/{z}/{x}/{y}.grid.json" ],
     "tiles": [ "http://localhost:20008/tile/demo_02/{z}/{x}/{y}.png" ],
+    "scale": 1,
+    "metatile": 2,
     "id": "demo_02",
     "interactivity": false,
     "maxzoom": 22,
diff --git a/test/fixtures/datasource-postgis.json b/test/fixtures/datasource-postgis.json
index d76515a..35a8f4c 100644
--- a/test/fixtures/datasource-postgis.json
+++ b/test/fixtures/datasource-postgis.json
@@ -8,5 +8,13 @@
     "geometry_type": "linestring",
     "id": "postgis",
     "project": "demo_01",
-    "type": "vector"
+    "type": "vector",
+    "unproj_extent" : [
+      -137.55088970390102,
+      -52.986907341349465,
+      140.97763013616188,
+      70.07531103545583
+    ],
+    "extent" : "-15312095,-6980576.5,15693558,11093272",
+    "sticky_options":{}
 }
\ No newline at end of file
diff --git a/test/fixtures/datasource-shp-features.json b/test/fixtures/datasource-shp-features.json
index 421e5d1..b47a88d 100644
--- a/test/fixtures/datasource-shp-features.json
+++ b/test/fixtures/datasource-shp-features.json
@@ -3203,7 +3203,7 @@
         "SUBREGION": { "max": 155, "min": 0, "type": "Number" },
         "UN": { "max": 894, "min": 4, "type": "Number" }
     },
-    "geometry_type": "multipolygon",
+    "geometry_type": "polygon",
     "id": "world",
     "project": "demo_01",
     "type": "vector",
diff --git a/test/fixtures/datasource-sqlite.json b/test/fixtures/datasource-sqlite.json
index 29b3345..5900cdb 100644
--- a/test/fixtures/datasource-sqlite.json
+++ b/test/fixtures/datasource-sqlite.json
@@ -30,5 +30,13 @@
     },
     "features": [],
     "type": "vector",
-    "geometry_type": "polygon"
+    "geometry_type": "polygon",
+    "unproj_extent" : [
+      -70.06163470944716,
+      12.41753750275562,
+      74.89230718349421,
+      38.47367493851016
+    ],
+    "extent" : "-7799225.5,1393264.125,8336973.5,4646558",
+    "sticky_options":{}
 }
diff --git a/test/project.test.js b/test/project.test.js
index 4964b70..c2c5650 100644
--- a/test/project.test.js
+++ b/test/project.test.js
@@ -1,6 +1,9 @@
 var assert = require('assert');
 var fs = require('fs');
 var path = require('path');
+var diff = require('difflet')({ indent : 2 });
+var core;
+var tile;
 
 function readJSON(name) {
     var json = fs.readFileSync(path.resolve(__dirname + '/fixtures/' + name + '.json'), 'utf8');
@@ -18,60 +21,75 @@ function cleanProject(proj) {
     if (Array.isArray(proj.grids)) proj.grids = proj.grids.map(removeTimestamp);
 }
 
-require('./support/start')(function(command) {
-    exports['test project collection endpoint'] = function() {
-        assert.response(command.servers['Core'],
-            { url: '/api/Project' },
-            { status: 200 },
-            function(res) {
-                var body = JSON.parse(res.body);
-                cleanProject(body[0]);
-                assert.deepEqual(readJSON('existing-project'), body[0]);
-            }
-        );
-    };
+describe('project', function() {
 
-    exports['test project endpoint'] = function(beforeExit) {
-        var completed = false;
-        assert.response(command.servers['Core'],
-            { url: '/api/Project/demo_01' },
-            { status: 200 },
-            function(res) {
-                var body = JSON.parse(res.body);
-                var _updated = body._updated;
-                cleanProject(body);
-                assert.deepEqual(readJSON('existing-project'), body);
+before(function(done) {
+    require('./support/start').start(function(command) {
+        core = command.servers['Core'];
+        tile = command.servers['Tile'];
+        done();
+    });
+});
 
-                // No new update.
-                assert.response(command.servers['Core'],
-                    { url: '/api/Project/demo_01/' + _updated },
-                    { body: '{}', status: 200 }
-                );
+after(function(done) {
+    core.close();
+    tile.close();
+    done();
+});
 
-                // Update notification.
-                assert.response(command.servers['Core'],
-                    { url: '/api/Project/demo_01/' + (+_updated - 1000) },
-                    { status: 200 },
-                    function(res) {
-                        var body = JSON.parse(res.body);
-                        var _updated = body._updated;
-                        cleanProject(body);
-                        assert.deepEqual(readJSON('existing-project'), body);
-                        completed = true;
-                    }
-                );
-            }
-        );
+it('collection should list projects', function(done) {
+    assert.response(core,
+        { url: '/api/Project' },
+        { status: 200 },
+        function(res) {
+            var body = JSON.parse(res.body);
+            cleanProject(body[0]);
+            assert.deepEqual(readJSON('existing-project'), body[0]);
+            done();
+        }
+    );
+});
 
-        beforeExit(function() {
-            assert.ok(completed);
-        })
-    };
+var _updated;
+it('model should load JSON', function(done) {
+    assert.response(core,
+        { url: '/api/Project/demo_01' },
+        { status: 200 },
+        function(res) {
+            var body = JSON.parse(res.body);
+            _updated = body._updated;
+            cleanProject(body);
+            assert.deepEqual(readJSON('existing-project'), body);
+            done();
+        }
+    );
+});
+it('model should not have an update', function(done) {
+    assert.response(core,
+        { url: '/api/Project/demo_01/' + _updated },
+        { body: '{}', status: 200 },
+        function(res) { done(); }
+    );
+});
+it('model should have an update', function(done) {
+    assert.response(core,
+        { url: '/api/Project/demo_01/' + (+_updated - 1000) },
+        { status: 200 },
+        function(res) {
+            var body = JSON.parse(res.body);
+            cleanProject(body);
+            assert.deepEqual(readJSON('existing-project'), body);
+            completed = true;
+            done();
+        }
+    );
+});
 
-    exports['test project creation'] = function(beforeExit) {
-        var completed = false;
-        var data = readJSON('create-project');
-        assert.response(command.servers['Core'], {
+it('PUT should create a project', function(done) {
+    var completed = false;
+    var data = readJSON('create-project');
+    assert.response(core,
+        {
             url: '/api/Project/demo_02',
             method: 'PUT',
             headers: {
@@ -79,87 +97,97 @@ require('./support/start')(function(command) {
                 'cookie': 'bones.token=' + data['bones.token']
             },
             data: JSON.stringify(data)
-        }, { status: 200 }, function(res) {
+        },
+        { status: 200 },
+        function(res) {
             var body = JSON.parse(res.body);
             cleanProject(body);
-            assert.deepEqual({
+            var expected = {
                 tiles: ["http://localhost:20008/tile/demo_02/{z}/{x}/{y}.png"],
                 grids: ["http://localhost:20008/tile/demo_02/{z}/{x}/{y}.grid.json"]
-            }, body);
-
-            assert.response(command.servers['Core'],
-                { url: '/api/Project/demo_02' },
-                { status: 200 },
-                function(res) {
-                    var body = JSON.parse(res.body);
-                    cleanProject(body);
-                    assert.deepEqual(readJSON('created-project'), body);
+            };
+            // https://github.com/mapbox/tilemill/issues/1552
+            //assert.deepEqual(expected, body, diff.compare(expected, body));
+            done();
+        }
+    );
+});
 
-                    assert.response(command.servers['Core'], {
-                        url: '/api/Project/demo_02',
-                        method: 'DELETE',
-                        headers: {
-                            'content-type': 'application/json',
-                            'cookie': 'bones.token=' + data['bones.token']
-                        },
-                        data: JSON.stringify({ 'bones.token': data['bones.token'] })
-                    }, { status: 200 }, function(res) {
-                        assert.equal(res.body, '{}');
-                        completed = true;
+it('GET should load created project', function(done) {
+    assert.response(core,
+        { url: '/api/Project/demo_02' },
+        { status: 200 },
+        function(res) {
+            var body = JSON.parse(res.body);
+            cleanProject(body);
+            var expected = readJSON('created-project');
+            // https://github.com/mapbox/tilemill/issues/1552
+            //assert.deepEqual(expected, body, diff.compare(expected, body));
+            done();
+        }
+    );
+});
 
-                        // We're done using the tile server at this point.
-                        command.servers['Tile'].close();
-                    });
-                }
-            );
-        });
+it('DELETE should remove project', function(done) {
+    var data = readJSON('create-project');
+    assert.response(core, {
+        url: '/api/Project/demo_02',
+        method: 'DELETE',
+        headers: {
+            'content-type': 'application/json',
+            'cookie': 'bones.token=' + data['bones.token']
+        },
+        data: JSON.stringify({ 'bones.token': data['bones.token'] })
+    }, { status: 200 }, function(res) {
+        assert.equal(res.body, '{}');
+        done();
+    });
+});
 
-        beforeExit(function() {
-            assert.ok(completed);
-        });
-    };
+it('PUT should fail with invalid id', function(done) {
+    var data = readJSON('create-project');
+    data.id = 'Bad !@!ID';
+    assert.response(core, {
+        url: '/api/Project/Bad%20!@!ID',
+        method: 'PUT',
+        headers: {
+            'content-type': 'application/json',
+            'cookie': 'bones.token=' + data['bones.token'],
+            'accept': 'application/json'
+        },
+        data: JSON.stringify(data)
+    }, { status: 409 }, function(res) {
+        var body = JSON.parse(res.body);
+        delete body.stack;
+        assert.deepEqual({
+            message: "Filename may include alphanumeric characters, dashes and underscores."
+        }, body);
+        assert['throws'](function() {
+            fs.statSync('./test/fixtures/files/project/Bad !@!ID');
+        }, "ENOENT, No such file or directory './test/fixtures/files/project/Bad !@!ID'");
+        done();
+    });
+});
 
-    exports['test project creation with invalid id'] = function() {
-        var data = readJSON('create-project');
-        data.id = 'Bad !@!ID';
-        assert.response(command.servers['Core'], {
-            url: '/api/Project/Bad%20!@!ID',
-            method: 'PUT',
-            headers: {
-                'content-type': 'application/json',
-                'cookie': 'bones.token=' + data['bones.token'],
-                'accept': 'application/json'
-            },
-            data: JSON.stringify(data)
-        }, { status: 409 }, function(res) {
-            var body = JSON.parse(res.body);
-            delete body.stack;
-            assert.deepEqual({
-                message: "Error: Filename may include alphanumeric characters, dashes and underscores."
-            }, body);
-            assert['throws'](function() {
-                fs.statSync('./test/fixtures/files/project/Bad !@!ID');
-            }, "ENOENT, No such file or directory './test/fixtures/files/project/Bad !@!ID'");
-        });
-    };
+it('PUT should fail with invalid stylesheet', function(done) {
+    var data = readJSON('invalid-project');
+    assert.response(core, {
+        url: '/api/Project/demo_01',
+        method: 'PUT',
+        headers: {
+            'content-type': 'application/json',
+            'cookie': 'bones.token=' + data['bones.token'],
+            'accept': 'application/json'
+        },
+        data: JSON.stringify(data)
+    }, { status: 409 }, function(res) {
+        var body = JSON.parse(res.body);
+        delete body.stack;
+        assert.deepEqual({
+            message: "style.mss:2:2 Invalid value for background-color, the type color is expected. blurb (of type keyword)  was given."
+        }, body);
+        done();
+    });
+});
 
-    exports['test updating project with invalid stylesheet'] = function() {
-        var data = readJSON('invalid-project');
-        assert.response(command.servers['Core'], {
-            url: '/api/Project/demo_01',
-            method: 'PUT',
-            headers: {
-                'content-type': 'application/json',
-                'cookie': 'bones.token=' + data['bones.token'],
-                'accept': 'application/json'
-            },
-            data: JSON.stringify(data)
-        }, { status: 409 }, function(res) {
-            var body = JSON.parse(res.body);
-            delete body.stack;
-            assert.deepEqual({
-                message: "Error: style.mss:2:2 Invalid value for background-color, a valid color is expected. blurb was given."
-            }, body);
-        });
-    };
 });
diff --git a/test/support/start.js b/test/support/start.js
index 65fbcda..25c3da7 100644
--- a/test/support/start.js
+++ b/test/support/start.js
@@ -1,8 +1,9 @@
 process.env.NODE_ENV = 'test';
 process.argv[2] = 'test';
 
-var Queue = require('../../lib/queue');
-var Step = require('step');
+var http = require('http');
+var util = require('util');
+var assert = require('assert');
 var exec = require('child_process').exec;
 var path = require('path');
 var fs = require('fs');
@@ -11,7 +12,7 @@ var basedir = path.resolve(__dirname + '/..');
 process.env.HOME = path.resolve(__dirname + '/../fixtures/files');
 
 // Remove stale config file if present.
-try { fs.unlinkSync(process.env.HOME + '/.tilemill.json'); }
+try { fs.unlinkSync(process.env.HOME + '/.tilemill/config.json'); }
 catch (err) { if (err.code !== 'ENOENT') throw err }
 
 // Load application.
@@ -20,31 +21,24 @@ var tilemill = require('bones').plugin;
 tilemill.config.files = path.resolve(__dirname + '/../fixtures/files');
 tilemill.config.examples = false;
 
-var queue = new Queue(function(next, done) {
-    // Allow bootstrap functions to be added to the queue.
-    if (next.bootstrap) return next(done);
-
-    var command = tilemill.start(function() {
-        var remaining = 2;
-        command.servers['Core'].close = (function(parent) { return function() {
-            if (remaining-- === 1) done();
-            return parent.apply(this, arguments);
-        }})(command.servers['Core'].close);
-        command.servers['Tile'].close = (function(parent) { return function() {
-            if (remaining-- === 1) done();
-            return parent.apply(this, arguments);
-        }})(command.servers['Tile'].close);
-        next(command);
+module.exports.start = function(done) {
+    // Create a clean environment.
+    var clean = '\
+        rm -f ' + basedir + '/fixtures/files/app.db && \
+        rm -rf ' + basedir + '/fixtures/files/project && \
+        rm -rf ' + basedir + '/fixtures/files/data && \
+        rm -rf ' + basedir + '/fixtures/files/export && \
+        cp -R ' + basedir + '/fixtures/pristine/project ' + basedir + '/fixtures/files';
+    exec(clean, function(err) {
+        if (err) throw err;
+        console.warn('Initialized test fixture');
+        var command = tilemill.start(function() {
+            done(command);
+        });
     });
-}, 1);
-// @TODO:
-// Not sure why this is necessary. tile.test.js doesn't seem to exit out
-// despite both servers closing.
-queue.on('empty', process.exit);
-
-// Insert postgis fixture only once for all tests as they will not be
-// modified. Queued first after which the remaining tests run.
-var postgis = function(callback) {
+};
+
+module.exports.startPostgis = function(done) {
     var insert = '\
         psql -d postgres -c "DROP DATABASE IF EXISTS tilemill_test;" && \
         createdb -E UTF8 -T template_postgis tilemill_test && \
@@ -52,28 +46,131 @@ var postgis = function(callback) {
     exec(insert, function(err) {
         if (err) throw err;
         console.warn('Inserted postgres fixture.');
-        callback();
+        module.exports.start(done);
     });
 };
-postgis.bootstrap = true;
-queue.add(postgis);
-
-tilemill.commands.test.augment({
-    bootstrap: function(parent, plugin, callback) {
-        // Create a clean environment.
-        var clean = '\
-            rm -f ' + basedir + '/fixtures/files/app.db && \
-            rm -rf ' + basedir + '/fixtures/files/project && \
-            rm -rf ' + basedir + '/fixtures/files/data && \
-            rm -rf ' + basedir + '/fixtures/files/export && \
-            cp -R ' + basedir + '/fixtures/pristine/project ' + basedir + '/fixtures/files';
-        exec(clean, function(err) {
-            if (err) throw err;
-            console.warn('Initialized test fixture');
-            parent.call(this, plugin, callback);
-        }.bind(this));
+
+/**
+ * Assert response from `server` with
+ * the given `req` object and `res` assertions object.
+ *
+ * @param {Server} server
+ * @param {Object} req
+ * @param {Object|Function} res
+ * @param {String} msg
+ */
+assert.response = function(server, req, res, msg) {
+    // Callback as third or fourth arg
+    var callback = typeof res === 'function'
+        ? res
+        : typeof msg === 'function'
+            ? msg
+            : function() {};
+
+    // Default message to test title
+    if (typeof msg === 'function') msg = null;
+    msg = msg || '';
+
+    // Issue request
+    var timer,
+        method = req.method || 'GET',
+        status = res.status || res.statusCode,
+        data = req.data || req.body,
+        requestTimeout = req.timeout || 0,
+        encoding = req.encoding || 'utf8';
+
+    var request = http.request({
+        host: '127.0.0.1',
+        port: server.port,
+        path: req.url,
+        method: method,
+        headers: req.headers
+    });
+
+    var check = function() {
+        if (--server.__pending === 0) {
+            server.close();
+            server.__listening = false;
+        }
+    };
+
+    // Timeout
+    if (requestTimeout) {
+        timer = setTimeout(function() {
+            check();
+            delete req.timeout;
+            throw new Error(msg + 'Request timed out after ' + requestTimeout + 'ms.');
+        }, requestTimeout);
     }
-});
 
-module.exports = queue.add;
+    if (data) request.write(data);
+
+    request.on('response', function(response) {
+        response.body = '';
+        response.setEncoding(encoding);
+        response.on('data', function(chunk) { response.body += chunk; });
+        response.on('end', function() {
+            if (timer) clearTimeout(timer);
+            // Assert response body
+            if (res.body !== undefined) {
+                var eql = res.body instanceof RegExp
+                  ? res.body.test(response.body)
+                  : res.body === response.body;
+                assert.ok(
+                    eql,
+                    msg + 'Invalid response body.\n'
+                        + '    Expected: ' + util.inspect(res.body) + '\n'
+                        + '    Got: ' + util.inspect(response.body)
+                );
+            }
+
+            // Assert response status
+            if (typeof status === 'number') {
+                assert.equal(
+                    response.statusCode,
+                    status,
+                    msg + 'Invalid response status code.\n'
+                        + '     Expected: ' + status + '\n'
+                        + '     Got: ' + response.statusCode + '' + response.body
+                );
+            }
+
+            // Assert response headers
+            if (res.headers) {
+                var keys = Object.keys(res.headers);
+                for (var i = 0, len = keys.length; i < len; ++i) {
+                    var name = keys[i],
+                        actual = response.headers[name.toLowerCase()],
+                        expected = res.headers[name],
+                        eql = expected instanceof RegExp
+                          ? expected.test(actual)
+                          : expected == actual;
+                        assert.ok(
+                            eql,
+                            msg + 'Invalid response header ' + name + '.\n'
+                                + '    Expected: ' + expected + '\n'
+                                + '    Got: ' + actual
+                        );
+                }
+            }
+            // Callback
+            callback(response);
+        });
+    });
+
+    request.end();
+
+};
+
+/**
+ * Assert that `str` matches `regexp`.
+ *
+ * @param {String} str
+ * @param {RegExp} regexp
+ * @param {String} msg
+ */
+assert.match = function(str, regexp, msg) {
+    msg = msg || util.inspect(str) + ' does not match ' + util.inspect(regexp);
+    assert.ok(regexp.test(str), msg);
+};
 
diff --git a/test/tile.test.js b/test/tile.test.js
index 8382cd4..3ad9c4b 100644
--- a/test/tile.test.js
+++ b/test/tile.test.js
@@ -1,38 +1,55 @@
 var assert = require('assert');
+var core;
+var tile;
 
-require('./support/start')(function(command) {
-    command.servers['Core'].close();
+describe('tile endpoint', function() {
 
-    exports['test non-existant tile endpoint'] = function() {
-        assert.response(command.servers['Tile'],
-            { url: '/tile/does_not_exist/0/0/0.png', encoding: 'binary' },
-            { body: /Project does not exist/, status: 404 }
-        );
-    };
+before(function(done) {
+    require('./support/start').start(function(command) {
+        core = command.servers['Core'];
+        tile = command.servers['Tile'];
+        done();
+    });
+});
+
+after(function(done) {
+    core.close();
+    tile.close();
+    done();
+});
 
-    exports['test tile endpoint'] = function() {
-        assert.response(command.servers['Tile'],
-            { url: '/tile/demo_01/2/2/1.png', encoding: 'binary' },
-            { status: 200 },
-            function(res) {
-                assert.equal(res.body.length, 18568);
-            }
-        );
-    };
+it('should 404 for missing project', function(done) {
+    assert.response(tile,
+        { url: '/tile/does_not_exist/0/0/0.png', encoding: 'binary' },
+        { body: /Project does not exist/, status: 404 },
+        function() { done(); }
+    );
+});
+it ('should 200 (tile) for existing project', function(done) {
+    assert.response(tile,
+        { url: '/tile/demo_01/2/2/1.png', encoding: 'binary' },
+        { status: 200 },
+        function(res) {
+            assert.equal(res.body.length, 17879);
+            done();
+        }
+    );
+});
+it ('should 200 (grid) for existing project', function(done) {
+    assert.response(tile,
+        { url: '/tile/demo_01/2/2/1.grid.json' },
+        { status: 200 },
+        function(res) {
+            function grid(data) { return data; };
+            var data = eval(res.body);
+            assert.equal(data.grid.length, 64);
+            assert.equal(data.keys.length, 90);
+            assert.equal(Object.keys(data.data).length, 89);
+            assert.equal(data.keys[1], '154');
+            assert.equal(data.data['154'].NAME, 'Norway');
+            done();
+        }
+    );
+});
 
-    exports['test grid endpoint'] = function() {
-        assert.response(command.servers['Tile'],
-            { url: '/tile/demo_01/2/2/1.grid.json' },
-            { status: 200 },
-            function(res) {
-                function grid(data) { return data; };
-                var data = eval(res.body);
-                assert.equal(data.grid.length, 64);
-                assert.equal(data.keys.length, 90);
-                assert.equal(Object.keys(data.data).length, 89);
-                assert.equal(data.keys[1], '154');
-                assert.equal(data.data['154'].NAME, 'Norway');
-            }
-        );
-    };
 });
diff --git a/tilemill.ico b/tilemill.ico
new file mode 100644
index 0000000..c1a7858
Binary files /dev/null and b/tilemill.ico differ
diff --git a/views/App.bones b/views/App.bones
index 57fcc18..21b29a8 100644
--- a/views/App.bones
+++ b/views/App.bones
@@ -14,7 +14,7 @@ Bones.utils.serial = function (steps, callback) {
 
 Bones.utils.form = function(form, model, options) {
     var parseOptions = function (o) {
-        return _(o.match(/([\d\w]*)\=(\"[^\"]*\"|[^\s]*)/g)).reduce(function(memo,pair) {
+        return _(o.match(/([\d\w-]*)\=(\"[^\"]*\"|[^\s]*)/g)).reduce(function(memo,pair) {
             pair = pair.replace(/"|'/g, '').split('=');
             memo[pair[0]] = pair[1];
             return memo;
@@ -99,7 +99,7 @@ view.prototype.events = {
     'click a.popup': 'popupOpen',
     'click #drawer a[href=#close]': 'drawerClose',
     'click a.drawer': 'drawerOpen',
-    'click .button.dropdown, .button.dropdown a': 'dropdown',
+    'click .button.dropdown': 'dropdown',
     'click .toggler a': 'toggler',
     'click a.restart': 'restart',
     'keydown': 'keydown'
@@ -214,10 +214,12 @@ view.prototype.dropdown = function(ev) {
     if (!target.hasClass('active')) {
         target.addClass('active');
         $(app).bind('click', collapse);
+        target.children('.menu').bind('click', collapse);
     }
     function collapse(ev) {
         target.removeClass('active');
         $(app).unbind('click', collapse);
+        target.children('.menu').bind('click', collapse);
     }
 };
 
diff --git a/views/Config.bones b/views/Config.bones
index e492cc4..9a98421 100644
--- a/views/Config.bones
+++ b/views/Config.bones
@@ -3,10 +3,14 @@ view = Backbone.View.extend();
 view.prototype.events = {
     'change input[name=updates]': 'updates',
     'change input[name=profile]': 'profile',
+    'change input[name=verbose]': 'verbose',
+    'change input[name=httpProxy]': 'proxy',
+    'keyup input[name=httpProxy]': 'proxy',
     'keyup input[name=files]': 'files',
     'change input[name=files]': 'files',
     'click input[type=submit]': 'save',
-    'click a[href=#disable]': 'disable'
+    'click a[href=#disable]': 'disable',
+    'click a[href="/oauth/mapbox"]': 'proxyWarning'
 };
 
 view.prototype.initialize = function(options) {
@@ -18,10 +22,14 @@ view.prototype.initialize = function(options) {
         'updates',
         'disable',
         'save',
-        'restart'
+        'restart',
+        'proxy',
+        'verbose'
     );
     this.model.bind('change', this.changed);
     this.model.bind('change:files', this.restart);
+    this.model.bind('change:httpProxy', this.restart);
+    this.model.bind('change:verbose', this.restart);
     this.render();
 };
 
@@ -46,6 +54,11 @@ view.prototype.files = function(ev) {
     return false;
 };
 
+view.prototype.proxy = function(ev) {
+    this.model.set({httpProxy: $(ev.currentTarget).val()});
+    return false;
+};
+
 view.prototype.updates = function(ev) {
     this.model.set({updates: $(ev.currentTarget).is(':checked')});
 };
@@ -54,6 +67,11 @@ view.prototype.profile = function(ev) {
     this.model.set({profile: $(ev.currentTarget).is(':checked')});
 };
 
+view.prototype.verbose = function(ev) {
+    var value = $(ev.currentTarget).is(':checked') ? 'on' : 'off';
+    this.model.set({verbose: value});
+};
+
 view.prototype.disable = function(ev) {
     this.model.set({
         'syncAccount': '',
@@ -87,4 +105,13 @@ view.prototype.restart = function() {
     this._restart = true;
 };
 
-
+view.prototype.proxyWarning = function() {
+    // Work around for lack of proxy support in topcube
+    if (this.model.get('httpProxy') && !this.model.get('server')) {
+        var view = new views.Modal({content: 'Unable to authorize with HTTP proxy.'});
+        var msg = 'To authorize, open this link in a proxy enabled browser: <br> ' + window.location.origin + '/oauth/mapbox';
+        view.el.children('.content').append($('<a href="/oauth/mapbox" target="_blank">'+msg+'</a>'));
+        view.el.children('.bug').remove();
+        return false;
+    }
+}
diff --git a/views/Layer.bones b/views/Layer.bones
index 75bc167..c6c438c 100644
--- a/views/Layer.bones
+++ b/views/Layer.bones
@@ -9,6 +9,8 @@ view.prototype.events = {
     'keyup input[name$=file], .layer-postgis textarea': 'placeholderUpdate',
     'change input[name$=file], .layer-postgis textarea': 'placeholderUpdate',
     'click a[href=#cacheFlush]': 'cacheFlush',
+    'change select[name=Datasource.extent_cache]': 'extentSelect',
+    'click a[href=#extentCacheFlush]': 'extentCacheFlush',
     'change select[name=srs-name]': 'nameToSrs',
     'keyup input[name=srs]': 'srsToName'
 };
@@ -24,6 +26,8 @@ view.prototype.initialize = function(options) {
         'favoriteUpdate',
         'placeholderUpdate',
         'cacheFlush',
+        'extentSelect',
+        'extentCacheFlush',
         'nameToSrs',
         'srsToName',
         'autoname'
@@ -48,6 +52,38 @@ view.prototype.render = function() {
     return this;
 };
 
+view.prototype.extentSelect = function(ev) {
+    var el = $(ev.currentTarget);
+    var name = el.val();
+    $('input[name="Datasource.extent"]').val('');
+    if (name == 'auto') {
+        $('a[href="#extentCacheFlush"]').css('display', 'inline-block');
+        $('small[for=auto]').css('display', 'block');
+    } else {
+        $('a[href="#extentCacheFlush"]').css('display', 'none');
+        $('small[for=auto]').css('display', 'none');
+    }
+
+    if (name == 'custom') {
+        $('input[name="Datasource.extent"]').css('display', 'inline');
+        $('small[for=custom]').css('display', 'block');
+    } else {
+        $('input[name="Datasource.extent"]').css('display', 'none');
+        $('small[for=custom]').css('display', 'none');
+    }
+
+    if (name == 'dynamic') {
+        $('small[for=dynamic]').css('display', 'block');
+    } else {
+        $('small[for=dynamic]').css('display', 'none');
+    }
+};
+
+view.prototype.extentCacheFlush = function(ev) {
+    $('input[name="Datasource.extent"]').val('');
+    return false;
+};
+
 view.prototype.nameToSrs = function(ev) {
     var el = $(ev.currentTarget);
     var name = $(ev.currentTarget).val();
@@ -86,7 +122,13 @@ view.prototype.favoriteUpdate = function(ev) {
     var target = $(ev.currentTarget);
     var favorite = target.siblings('a.favorite');
     var uri = target.val();
-    if (uri.match(/^(\/|http:\/\/|(.+\s)?dbname=[\w]+)/)) {
+    var match;
+    if (window.abilities.platform === 'win32') {
+       match = uri.match(/^(\/|\\|[\w]:\\|http:\/\/|(.+\s)?dbname=[\w]+)/);
+    } else {
+       match = uri.match(/^(\/|http:\/\/|(.+\s)?dbname=[\w]+)/);
+    }
+    if (match) {
         favorite.removeClass('hidden');
         if (this.favorites.isFavorite(uri)) {
             favorite.addClass('active');
@@ -118,7 +160,8 @@ view.prototype.placeholderUpdate = function(ev) {
 // @TODO smarter handling for this or abandon the idea if it turns out to be
 // untenable for queries.
 view.prototype.autoname = function(source) {
-    return _(source.split('/')).chain()
+    var sep = window.abilities.platform === 'win32' ? '\\' : '/';
+    return _(source.split(sep)).chain()
         .map(function(chunk) { return chunk.split('\\'); })
         .flatten()
         .last()
@@ -139,8 +182,14 @@ view.prototype.browse = function(ev) {
         if (form.hasClass('layer-sqlite')) return 'sqlite';
         if (form.hasClass('layer-postgis')) return 'favoritesPostGIS';
     })(form);
-    var components = $('input.browsable', form).val().split('/');
-    var location = components.slice(0, components.length - 1).join('/');
+    var location = $('input.browsable', form).val();
+    if (location) {  // detect if the path is a file so we can browse its directory
+        var sep = window.abilities.platform === 'win32' ? '\\' : '/';
+        var components = location.split(sep);
+        if (components && (components[components.length - 1][0] != '.') && location.match(/\.([0-9a-z]+)(?:[\?#]|$)/i)) {
+            location = components.slice(0, components.length - 1).join(sep);
+        }
+    }
 
     target
         .toggleClass('active')
@@ -169,12 +218,15 @@ view.prototype.autostyle = function() {
     var root = this.model.collection.parent;
     var stylesheets = root.get('Stylesheet');
     if (stylesheets.length !== 0) {
-        var cm = stylesheets.models[0].codemirror;
-        var coord = cm.coordsFromIndex(Infinity);
-        cm.replaceRange(
-            templates.Autostyle(this.model),
-            coord,
-            coord);
+        var cm = stylesheets.models[$('.tabs .tab.active').parent().index()].codemirror;
+        if (cm) {
+            // codemirror >= 2.2 uses posFromIndex
+            var coord = cm.posFromIndex ? cm.posFromIndex(Infinity) : cm.coordsFromIndex(Infinity);
+            cm.replaceRange(
+                templates.Autostyle(this.model),
+                coord,
+                coord);
+        }
         $('.actions a[href=#save]').click();
     }
 };
@@ -192,10 +244,16 @@ view.prototype.save = function(e) {
         attr.srs = attr.srs || '';
     }
     // Advanced options.
-    if (attr.advanced) {
-        attr.Datasource = _(attr.Datasource||{}).defaults(attr.advanced);
-        delete attr.advanced;
-    }
+    var regular = _(['type', 'file','table', 'host', 'port', 'user', 
+        'password', 'dbname', 'extent', 'key_field', 'geometry_field',
+        'type', 'attachdb', 'srs', 'id', 'project', 'extent_cache']);
+
+    var result = {};
+    _(attr.Datasource || {}).each(function(v, k) {
+        if (regular.include(k)) result[k] = v;
+    })
+    attr.Datasource = _.extend(result, attr.advanced);
+
     // Parse PostGIS connection options.
     if (attr.connection) {
         var allowedArgs = ['user', 'password', 'dbname', 'port', 'host'];
@@ -226,8 +284,16 @@ view.prototype.save = function(e) {
         new views.Modal(e);
     }).bind(this);
     this.model.validateAsync(attr, {
-        success: _(function() {
+        success: _(function(model, resp) {
             $(this.el).removeClass('loading').removeClass('restartable');
+            if (attr.Datasource && attr.Datasource.extent_cache === 'auto') {
+                attr.Datasource.extent = resp.extent;
+            }
+            if (resp.sticky_options) {
+                Object.keys(resp.sticky_options).forEach(function(opt) {
+                    attr.Datasource[opt] = resp.sticky_options[opt];
+                });
+            }
             if (!this.model.set(attr, {error:error})) return;
             if (!this.model.collection.include(this.model)) {
                 this.model.collection.add(this.model);
diff --git a/views/Layers.bones b/views/Layers.bones
index 923e179..963bef6 100644
--- a/views/Layers.bones
+++ b/views/Layers.bones
@@ -5,7 +5,9 @@ view.prototype.events = {
     'click a.add-layer': 'layerAdd',
     'click a.edit': 'layerEdit',
     'click a.inspect': 'layerInspect',
-    'click a.delete': 'layerDelete'
+    'click a.delete': 'layerDelete',
+    'click a.visibility': 'layerToggleStatus',
+    'click a.extent': 'layerExtent'
 };
 
 view.prototype.initialize = function(options) {
@@ -15,6 +17,8 @@ view.prototype.initialize = function(options) {
         'layerInspect',
         'layerEdit',
         'layerDelete',
+        'layerToggleStatus',
+        'layerExtent',
         'makeLayer',
         'sortLayers'
     );
@@ -86,11 +90,67 @@ view.prototype.layerDelete = function(ev) {
         callback: _(function() {
             var model = this.model.get('Layer').get(id);
             this.model.get('Layer').remove(model);
+
+            // Disable interactivity if on for this layer
+            var interactivity = this.model.get('interactivity');
+            if (interactivity.layer == id) {
+                interactivity.layer = "";
+                this.model.set({
+                    interactivity: interactivity
+                });
+            }
+
         }).bind(this),
         affirmative: 'Delete'
     });
     return false;
 };
+ 
+view.prototype.layerToggleStatus = function(ev) {
+    var id = $(ev.currentTarget).attr('href').split('#').pop();
+    var model = this.model.get('Layer').get(id);
+    if (model.get('status') == 'off') {
+        model.unset('status');
+        $(ev.currentTarget).closest('li').removeClass('status-off');
+    } else {
+        // default to hiding, since the default state of a layer is 'on'
+        model.set({ 'status': 'off' });
+        $(ev.currentTarget).closest('li').addClass('status-off');
+    }
+    return false;
+};
+
+view.prototype.layerExtent = function(ev) {
+    var id = $(ev.currentTarget).attr('href').split('#').pop();
+    var layer = this.model.get('Layer').get(id);
+    var extent = layer.get('extent');
+
+    var setExtent = _(function(extent) {
+        this.options.map.map.setExtent(
+                new MM.Extent(extent[3], extent[0], extent[1], extent[2]));
+        }).bind(this);
+
+    if (extent) {
+        setExtent(extent);
+    } else {
+        // Extent not yet set (layer saved prior to 0.9.2). Setting it.
+        var model = new models.Datasource(_(layer.get('Datasource')).extend({
+            id: layer.get('id'),
+            project: this.model.get('id'),
+            srs: layer.get('srs')
+        }));
+        model.fetch({
+            success: function(model, resp) {
+                layer.set({extent: resp.unproj_extent});
+                setExtent(resp.unproj_extent);
+            },
+            error: function(err) {
+                new views.Modal(err);
+            }
+        });
+    }
+    return false;
+}
 
 view.prototype.layerInspect = function(ev) {
     $('#drawer .content').empty();
@@ -102,10 +162,7 @@ view.prototype.layerInspect = function(ev) {
     var model = new models.Datasource(_(layer.get('Datasource')).extend({
         id: layer.get('id'),
         project: this.model.get('id'),
-        // millstone will not allow `srs` be undefined for inspection so we set
-        // it to null. We could use the layer's SRS, but this likely has fewer
-        // side effects.
-        srs: null
+        srs: layer.get('srs')
     }));
     model.fetchFeatures({
         success: _(function(model) {
diff --git a/views/Library.bones b/views/Library.bones
index 1719a4a..018cc70 100644
--- a/views/Library.bones
+++ b/views/Library.bones
@@ -85,6 +85,7 @@ view.prototype.libraryUpdate = function() {
 
 view.prototype.libraryLocation = function(ev) {
     var location = $(ev.currentTarget).attr('href').split('#').pop();
+    this.change(location);
     this.model.set({location:location});
     this.model.fetch({
         success:this.render,
diff --git a/plugins/map/views/Map.bones b/views/Map.bones
similarity index 72%
rename from plugins/map/views/Map.bones
rename to views/Map.bones
index ef27efd..0f6b468 100644
--- a/plugins/map/views/Map.bones
+++ b/views/Map.bones
@@ -20,10 +20,27 @@ view.prototype.render = function(init) {
     this.map = new MM.Map('map',
         new wax.mm.connector(this.model.attributes));
 
+    // Adapted location interaction - opens in new tab
+    function locationOn(o) {
+        if ((o.e.type === 'mousemove' || !o.e.type)) {
+            return;
+        } else {
+            var loc = o.formatter({ format: 'location' }, o.data);
+            if (loc) {
+                window.open(loc);
+            }
+        }
+    }
+
     // Add references to all controls onto the map object.
     // Allows controls to be removed later on.
     this.map.controls = {
-        interaction: wax.mm.interaction(this.map, this.model.attributes),
+        interaction: wax.mm.interaction()
+            .map(this.map)
+            .tilejson(this.model.attributes)
+            .on(wax.tooltip()
+                .parent(this.map.parent).events())
+            .on({on: locationOn}),
         legend: wax.mm.legend(this.map, this.model.attributes),
         zoombox: wax.mm.zoombox(this.map),
         zoomer: wax.mm.zoomer(this.map).appendTo(this.map.parent),
@@ -33,8 +50,8 @@ view.prototype.render = function(init) {
     // Add image error request handler. "Dedupes" image errors by
     // checking against last received image error so as to not spam
     // the user with the same errors message for every image request.
-    this.map.getLayerAt(0).requestManager.addCallback('requesterror', _(function(manager, url) {
-        $.ajax(url, { error: _(function(resp) {
+    this.map.getLayerAt(0).requestManager.addCallback('requesterror', _(function(manager, msg) {
+        $.ajax(msg.url, { error: _(function(resp) {
             if (resp.responseText === this._error) return;
             this._error = resp.responseText;
             new views.Modal(resp);
@@ -46,6 +63,9 @@ view.prototype.render = function(init) {
         center[1],
         center[0]),
         center[2]);
+    this.map.setZoomRange(
+        this.model.get('minzoom'),
+        this.model.get('maxzoom'));
     this.map.addCallback('zoomed', this.mapZoom);
     this.map.addCallback('panned', this.mapZoom);
     this.map.addCallback('extentset', this.mapZoom);
@@ -65,6 +85,7 @@ view.prototype.fullscreen = function(e) {
     } else {
         $('div.project').removeClass('fullscreen');
     }
+    this.map.draw();
 };
 
 // Set zoom display.
@@ -81,10 +102,13 @@ view.prototype.attach = function() {
     layer.provider.options.maxzoom = this.model.get('maxzoom');
     layer.setProvider(layer.provider);
 
-    this.map.controls.interaction.remove();
-    this.map.controls.interaction = wax.mm.interaction(
-        this.map,
-        this.model.attributes);
+    layer.provider.setZoomRange(layer.provider.options.minzoom,
+                          layer.provider.options.maxzoom)
+
+    this.map.setZoomRange(layer.provider.options.minzoom,
+                          layer.provider.options.maxzoom)
+
+    this.map.controls.interaction.tilejson(this.model.attributes);
 
     if (this.model.get('legend')) {
         this.map.controls.legend.content(this.model.attributes);
@@ -92,14 +116,7 @@ view.prototype.attach = function() {
     } else {
         $(this.map.controls.legend.element()).remove();
     }
-};
 
-// Hook in to project view with an augment.
-views.Project.augment({ render: function(p) {
-    p.call(this);
-    new views.Map({
-        el:this.$('.map'),
-        model:this.model
-    });
-    return this;
-}});
+    this.map.draw();
+    this.mapZoom();
+};
diff --git a/views/Metadata.bones b/views/Metadata.bones
index 1386df6..c8a807f 100644
--- a/views/Metadata.bones
+++ b/views/Metadata.bones
@@ -60,12 +60,20 @@ view.prototype.close = function() {
 };
 
 view.prototype.render = function() {
-    $(this.el).html(templates.Metadata(this));
+    if (this.model.get('format') !== 'sync' ||
+        (this.config.get('syncAccount') && this.config.get('syncAccessToken'))) {
+        $(this.el).html(templates.Metadata(this));
+    } else {
+        $(this.el).html(templates.MetadataSignup(this));
+    }
 
-    this.model.set({zooms:[
-        this.project.get('minzoom'),
-        this.project.get('maxzoom')
-    ]}, {silent:true});
+    this.model.set({
+        zooms: [
+            this.project.get('minzoom'),
+            this.project.get('maxzoom')
+        ],
+        metatile: this.project.get('metatile')
+    }, {silent:true});
     Bones.utils.sliders(this.$('.slider'), this.model);
 
     var center = this.project.get('center');
@@ -74,11 +82,15 @@ view.prototype.render = function() {
         new MM.Location(bounds[1], bounds[0]),
         new MM.Location(bounds[3], bounds[2])
     ];
-    // Override project attributes to allow unbounded zooming.
     var tj = _(this.project.attributes).clone();
     tj.minzoom = 0;
     tj.maxzoom = 22;
     this.map = new MM.Map('meta-map', new wax.mm.connector(tj));
+    
+    // Override project attributes to allow unbounded zooming.
+    this.map.setZoomRange(
+        tj.minzoom,
+        tj.maxzoom);
 
     wax.mm.zoomer(this.map).appendTo(this.map.parent);
     this.map.setExtent(extent);
@@ -163,12 +175,30 @@ view.prototype.updateTotal = function(attributes) {
     })(total));
     this.$('.totalsize').text((function(num) {
         num = num || 0;
-        if (num >= 1e12) return '1000 GB+';
-        if (num >= 1e10) return '100 GB+';
-        if (num >= 1e9) return '1 GB+';
-        if (num >= 1e8) return '100 MB+';
-        if (num >= 1e7) return '10 MB+';
-        if (num >= 1e6) return '1 MB+';
+        if (num >= 1e12) {
+            this.$('.totalsize').addClass('warning-red');
+            return '1000 GB+ reducing zoom level recommended';
+        }
+        if (num >= 1e10) {
+            this.$('.totalsize').addClass('warning-red');
+            return '100 GB+ reducing zoom level recommended';
+        }
+        if (num >= 1e9) {
+            this.$('.totalsize').addClass('warning-red');
+            return '1 GB+ reducing zoom level recommended';
+        }
+        if (num >= 1e8) {
+            this.$('.totalsize').removeClass('warning-red');
+            return '100 MB+';
+        }
+        if (num >= 1e7) {
+            this.$('.totalsize').removeClass('warning-red');
+            return '10 MB+';
+        }
+        if (num >= 1e6) {
+            this.$('.totalsize').removeClass('warning-red');
+            return '1 MB+';
+        }
         return '1 MB';
     })(total * 1000));
 };
diff --git a/views/Modal.bones b/views/Modal.bones
index ac5a8bb..fb3569d 100644
--- a/views/Modal.bones
+++ b/views/Modal.bones
@@ -18,9 +18,12 @@ view.prototype.initialize = function(options) {
 
     // Attempt to handle jqXHR objects.
     if (options.responseText) {
+        console.log(options.responseText);
         try {
+            var message = JSON.parse(options.responseText).message
+            if (message == undefined) throw new Error("");
             options = {
-              content: JSON.parse(options.responseText).message
+              content: message
             };
         } catch(e) {
             options = {
@@ -28,7 +31,12 @@ view.prototype.initialize = function(options) {
             };
         }
     } else if (options.status === 0) {
-        options = { content: 'No response from server.' };
+        var content = '';
+        if (options.err_message) {
+           content += options.err_message + ' : ';
+        }
+        content += 'Unable to reach the local TileMill Server. Check the logs for details. If this problem persists please contact support at: http://support.mapbox.com/discussions/tilemill';
+        options = { content: content };
     } else if (typeof options === 'string') {
         options = { content: options };
     } else if (options instanceof Error) {
diff --git a/views/Plugins.bones b/views/Plugins.bones
index c78c33e..7e73eb0 100644
--- a/views/Plugins.bones
+++ b/views/Plugins.bones
@@ -2,7 +2,8 @@ view = Backbone.View.extend();
 
 view.prototype.events = {
     'click a.install': 'npm',
-    'click a.uninstall': 'npm'
+    'click a.uninstall': 'npm',
+    'click a.upgrade': 'npm'
 };
 
 view.prototype.initialize = function(options) {
@@ -18,15 +19,17 @@ view.prototype.plugins = function() {
     this.$('.available').addClass('loading');
     this.available.fetch({
         success: _(function(m) {
+            // Stop if plugins page is no longer current page
+            if ($('#page').find('.plugins').length === 0) return;
             this.$('.available').removeClass('loading');
-            var drawn = m.map(_(function(plugin) {
-                if (this.collection.get(plugin.id)) return;
-                this.$('.available ul.grid').append(templates.Plugin(plugin));
-                return true;
-            }).bind(this));
-            if (!_(drawn).compact().length) {
-                this.$('.available ul.grid').replaceWith('<div class="empty description">No plugins found.</div>');
-            }
+            // Add latest version info the install plugins
+            this.collection.map(function(i) {
+                var avail = m.get(i.id);
+                if (avail) i.set({ latest: avail.get('dist-tags').latest});
+                return m;
+            });
+            // Re-render entire pane to add upgrade buttons
+            this.el.html(templates.Plugins(this));
         }).bind(this),
         error: _(function(m, err) {
             // If server is restarting, just stop. The page
@@ -81,7 +84,7 @@ view.prototype.npm = function(ev) {
     _(Bones.intervals||[]).each(clearInterval);
 
     $('body').addClass('loading');
-    if ($(ev.currentTarget).hasClass('install')) {
+    if ($(ev.currentTarget).hasClass('install') || $(ev.currentTarget).hasClass('upgrade')) {
         new models.Plugin({id:id}).save({}, options);
     } else {
         new models.Plugin({id:id}).destroy(options);
diff --git a/views/Preview.bones b/views/Preview.bones
index f4a69c7..46e5a60 100644
--- a/views/Preview.bones
+++ b/views/Preview.bones
@@ -27,7 +27,10 @@ view.prototype.render = function() {
     if (!MM) throw new Error('ModestMaps not found.');
     this.map = new MM.Map('preview',
         new wax.mm.connector(this.preview.attributes));
-    wax.mm.interaction(this.map, this.preview.attributes);
+    wax.mm.interaction()
+        .map(this.map)
+        .tilejson(this.preview.attributes)
+        .on(wax.tooltip().parent(this.map.parent).events());
     wax.mm.legend(this.map, this.preview.attributes).appendTo(this.map.parent);
     wax.mm.zoombox(this.map);
     wax.mm.zoomer(this.map).appendTo(this.map.parent);
@@ -38,6 +41,10 @@ view.prototype.render = function() {
         center[0]),
         center[2]);
 
+    this.map.setZoomRange(
+        this.model.get('minzoom'),
+        this.model.get('maxzoom'));
+
     return this;
 };
 
diff --git a/views/Project.bones b/views/Project.bones
index 7c78e06..fbf49e2 100644
--- a/views/Project.bones
+++ b/views/Project.bones
@@ -24,6 +24,7 @@ view.prototype.initialize = function() {
         'unload'
     );
     Bones.intervals = Bones.intervals || {};
+
     if (Bones.intervals.project) clearInterval(Bones.intervals.project);
     Bones.intervals.project = setInterval(_(function() {
         if (!$('.project').size()) return;
@@ -32,6 +33,32 @@ view.prototype.initialize = function() {
             clearInterval(Bones.intervals.project);
         }});
     }).bind(this), 1000);
+    this.dots = '.'
+    this.project_checks = 0;
+    if (Bones.intervals.projectTile) clearInterval(Bones.intervals.projectTile);
+    Bones.intervals.projectTile = setInterval(_(function() {
+        if (!$('.project').size()) return;
+        this.model.pollTileServer({
+            success: _(function(m, resp) {
+                if (resp && resp.status) {
+                    var name = resp.status+this.dots;
+                    $('.workspace .project-status').text(name);
+                    this.dots += '.'
+                    if (this.dots.split('.').length > 5)
+                       this.dots = '.';
+                } else {
+                    $('.workspace .project-status').text('');
+                    this.project_checks++;
+                    if (this.project_checks > 2) clearInterval(Bones.intervals.projectTile);
+                }
+            }).bind(this),
+            error: _(function(m, resp) {
+                $('.workspace .project-status').text('');
+                clearInterval(Bones.intervals.projectTile);
+            }).bind(this)
+        });
+    }).bind(this), 1000);
+
     window.onbeforeunload = window.onbeforeunload || this.unload;
 
     this.model.bind('error', this.error);
@@ -49,6 +76,13 @@ view.prototype.render = function(init) {
         .removeClass('disabled')
         .attr('href', '#/project/' + this.model.id);
     $(this.el).html(templates.Project(this.model));
+
+    // Create map
+    this.map = new views.Map({
+        el: this.$('.map'),
+        model: this.model
+    });
+
     return this;
 };
 
@@ -114,7 +148,6 @@ view.prototype.exportAdd = function(ev) {
             this.$('.project').removeClass('meta');
             if (!$('#drawer').is('.active')) {
                 $('a[href=#exports]').click();
-                $('.actions > .dropdown').click();
             }
             this.exportList();
         }).bind(this),
@@ -151,7 +184,8 @@ view.prototype.exportList = function(ev) {
 view.prototype.layers = function(ev) {
     new views.Layers({
         el: $('#drawer'),
-        model: this.model
+        model: this.model,
+        map: this.map
     });
 };
 

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/tilemill.git



More information about the Pkg-grass-devel mailing list