[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