├── test
├── support
│ └── resources
│ │ ├── +circle.svg
│ │ └── circle.svg
├── README.md
├── grainstore.js
├── style_trans_error.js
├── mml_store.js
├── mml_builder_pool.js
├── style_trans.js
├── mml_builder_multilayer.js
├── style_trans_23_to_30.js
└── mml_builder.js
├── .gitignore
├── .travis.yml
├── lib
└── grainstore
│ ├── index.js
│ ├── mml-builder
│ ├── mml-builder-child.js
│ ├── mml-builder.js
│ └── mml-builder-inline.js
│ ├── style_trans.js
│ ├── mml_store.js
│ └── paths
│ ├── from23To30.js
│ └── from20To21.js
├── .eslintrc.js
├── CONTRIBUTING.md
├── HOWTO_RELEASE
├── tools
├── style_filter
└── reset_styles
├── LICENCE
├── package.json
├── README.md
└── NEWS.md
/test/support/resources/+circle.svg:
--------------------------------------------------------------------------------
1 | circle.svg
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules*
2 | .idea
3 | test.log
4 | test/support/redis.pid
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 |
3 | language: node_js
4 | node_js:
5 | - "12"
6 |
--------------------------------------------------------------------------------
/test/README.md:
--------------------------------------------------------------------------------
1 | Tests
2 | -----
3 | To run the tests, from the project root:
4 |
5 | ```
6 | > npm install
7 | > npm test
8 | ```
9 |
--------------------------------------------------------------------------------
/lib/grainstore/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var version = require('../../package.json').version;
4 |
5 | module.exports = {
6 | version: function () {
7 | return version;
8 | },
9 | MMLStore: require('./mml_store'),
10 | MMLBuilder: require('./mml-builder/mml-builder')
11 | };
12 |
--------------------------------------------------------------------------------
/test/grainstore.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var assert = require('assert');
4 | var grainstore = require('../lib/grainstore');
5 |
6 | describe('grainstore', function () {
7 | it('version', function () {
8 | var version = grainstore.version();
9 | assert.equal(typeof (version), 'string');
10 | assert.equal(version, require('../package.json').version);
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | commonjs: true,
4 | es6: true,
5 | node: true,
6 | mocha: true
7 | },
8 | extends: [
9 | 'standard'
10 | ],
11 | globals: {
12 | Atomics: 'readonly',
13 | SharedArrayBuffer: 'readonly'
14 | },
15 | parserOptions: {
16 | ecmaVersion: 2018
17 | },
18 | rules: {
19 | "indent": ["error", 4],
20 | "semi": ["error", "always"]
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Contributing
2 | ---
3 |
4 | The issue tracker is at [github.com/CartoDB/grainstore](https://github.com/CartoDB/grainstore).
5 |
6 | We love pull requests from everyone, see [Contributing to Open Source on GitHub](https://guides.github.com/activities/contributing-to-open-source/#contributing).
7 |
8 |
9 | ## Submitting Contributions
10 |
11 | * You will need to sign a Contributor License Agreement (CLA) before making a submission. [Learn more here](https://cartodb.com/contributing).
12 |
--------------------------------------------------------------------------------
/test/support/resources/circle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
--------------------------------------------------------------------------------
/HOWTO_RELEASE:
--------------------------------------------------------------------------------
1 | 1. Test (make clean all check), fix if broken before proceeding
2 | 2. Ensure proper version in package.json
3 | 3. Ensure NEWS section exists for the new version, review it, add release date
4 | 4. Commit package.json, NEWS
5 | 5. git tag -a Major.Minor.Patch # use NEWS section as content
6 | 6. npm publish
7 | 7. Stub NEWS/package for next version
8 |
9 | Versions:
10 |
11 | Bugfix releases increment Patch component of version.
12 | Feature releases increment Minor and set Patch to zero.
13 | If backward compatibility is broken, increment Major and
14 | set to zero Minor and Patch.
15 |
16 | Branches named 'b.' are kept for any critical
17 | fix that might need to be shipped before next feature release
18 | is ready.
19 |
--------------------------------------------------------------------------------
/tools/style_filter:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var path = require('path');
4 | var fs = require('fs');
5 | var StyleTrans = require('../lib/grainstore/style_trans');
6 |
7 | function usage(me, exitcode) {
8 | console.log("Usage: " + me + " [from_version] [to_version]");
9 | process.exit(exitcode);
10 | }
11 |
12 | var node_path = process.argv.shift();
13 | var script_path = process.argv.shift();
14 | var me = path.basename(script_path);
15 | if ( process.argv.length < 2 ) usage(me, 1);
16 | var from_version = process.argv.shift();
17 | var to_version = process.argv.shift();
18 |
19 | var transformer = new StyleTrans();
20 |
21 | var fromstyle = fs.readFileSync('/dev/stdin').toString();
22 | var tostyle = transformer.transform(fromstyle, from_version, to_version);
23 | console.log(tostyle);
24 |
25 |
--------------------------------------------------------------------------------
/lib/grainstore/mml-builder/mml-builder-child.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _ = require('underscore');
4 | var debug = require('debug')('grainstore:mml-builder-child');
5 | var MMLBuilder = require('./mml-builder-inline');
6 |
7 | process.on('message', function (input) {
8 | debug('Child got MML Builder Context');
9 | try {
10 | var mmlBuilder = new MMLBuilder(input.context.params, input.context.options);
11 |
12 | // setting mml_builder context from parent process to have updated params before building XML
13 | _.extend(mmlBuilder, input.context);
14 |
15 | mmlBuilder.toXML(function (err, xml) {
16 | if (err) {
17 | return process.send({ err: err.message });
18 | }
19 |
20 | process.send({ xml: xml });
21 | });
22 | } catch (err) {
23 | return process.send({ err: err.message });
24 | }
25 | });
26 |
--------------------------------------------------------------------------------
/test/style_trans_error.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var assert = require('assert');
4 | var StyleTrans = require('../lib/grainstore/style_trans.js');
5 |
6 | describe('Style Transformer fails', function () {
7 | beforeEach(function () {
8 | this.styleTrans = new StyleTrans();
9 | });
10 |
11 | var polygonSuite = [{
12 | description: 'should return the same style from 2.0.0 to 2.0.2',
13 | from: '2.0.0',
14 | to: '2.0.2',
15 | input: [
16 | 'this',
17 | ' is not a valid',
18 | 'carto-css'
19 | ].join('\n'),
20 | expected: [
21 | 'this',
22 | ' is not a valid',
23 | 'carto-css'
24 | ].join('\n')
25 | }, {
26 | description: 'should return the same style from 2.3.0 to 3.0.12',
27 | from: '2.3.0',
28 | to: '3.0.12',
29 | input: [
30 | 'this',
31 | ' is not a valid',
32 | 'carto-css'
33 | ].join('\n'),
34 | expected: [
35 | 'this',
36 | ' is not a valid',
37 | 'carto-css'
38 | ].join('\n')
39 | }];
40 |
41 | var scenarios = [].concat(polygonSuite);
42 |
43 | scenarios.forEach(function (scenario) {
44 | it(scenario.description, function () {
45 | var outputStyle = this.styleTrans.transform(scenario.input, scenario.from, scenario.to);
46 | assert.equal(outputStyle, scenario.expected);
47 | });
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015, CartoDB
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | 1. Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | 2. Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | 3. Neither the name of the copyright holder nor the names of its contributors
15 | may be used to endorse or promote products derived from this software without
16 | specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/test/mml_store.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var assert = require('assert');
4 | var _ = require('underscore');
5 | var grainstore = require('../lib/grainstore');
6 |
7 | var DEFAULT_POINT_STYLE = [
8 | '#layer {',
9 | ' marker-fill: #FF6600;',
10 | ' marker-opacity: 1;',
11 | ' marker-width: 16;',
12 | ' marker-line-color: white;',
13 | ' marker-line-width: 3;',
14 | ' marker-line-opacity: 0.9;',
15 | ' marker-placement: point;',
16 | ' marker-type: ellipse;',
17 | ' marker-allow-overlap: true;',
18 | '}'
19 | ].join('');
20 |
21 | describe('mml_store', function () {
22 | it('can create new instance of mml_store', function () {
23 | var mmlStore = new grainstore.MMLStore();
24 | assert.ok(_.functions(mmlStore).indexOf('mml_builder') >= 0, "mml_store doesn't include 'mml_builder'");
25 | });
26 |
27 | it('cannot create new mml_builders with blank opts', function () {
28 | var mmlStore = new grainstore.MMLStore();
29 | assert.throws(function () {
30 | mmlStore.mml_builder();
31 | }, Error, 'Options must include dbname and table');
32 | });
33 |
34 | it('can create new mml_builders with normal ops', function (done) {
35 | var mmlStore = new grainstore.MMLStore();
36 | mmlStore.mml_builder({ dbname: 'my_database', sql: 'select * from whatever', style: DEFAULT_POINT_STYLE }).toXML(done);
37 | });
38 |
39 | it('can create new mml_builders with normal ops and sql', function (done) {
40 | var mmlStore = new grainstore.MMLStore();
41 | mmlStore.mml_builder({ dbname: 'my_database', sql: 'select * from whatever', style: DEFAULT_POINT_STYLE }).toXML(done);
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "grainstore",
3 | "version": "3.0.0",
4 | "main": "./lib/grainstore/index.js",
5 | "description": "Stores map styles and generates postgis friendly MML & XML for Mapnik",
6 | "keywords": [
7 | "cartodb"
8 | ],
9 | "url": "https://github.com/CartoDB/grainstore",
10 | "license": "BSD-3-Clause",
11 | "repository": {
12 | "type": "git",
13 | "url": "git://github.com/CartoDB/grainstore.git"
14 | },
15 | "author": "Vizzuality (http://vizzuality.com)",
16 | "contributors": [
17 | "Simon Tokumine ",
18 | "Sandro Santilli ",
19 | "Daniel García Aubert "
20 | ],
21 | "dependencies": {
22 | "carto": "0.16.3",
23 | "debug": "~3.1.0",
24 | "generic-pool": "~2.2.0",
25 | "millstone": "github:cartodb/millstone#v0.6.17-carto.4",
26 | "postcss": "~5.2.8",
27 | "postcss-scss": "0.4.0",
28 | "postcss-strip-inline-comments": "0.1.5",
29 | "semver": "~5.0.3",
30 | "underscore": "~1.6.0"
31 | },
32 | "devDependencies": {
33 | "eslint": "^6.8.0",
34 | "eslint-config-standard": "^14.1.1",
35 | "eslint-plugin-import": "^2.20.2",
36 | "eslint-plugin-node": "^11.1.0",
37 | "eslint-plugin-promise": "^4.2.1",
38 | "eslint-plugin-standard": "^4.0.1",
39 | "mocha": "^7.1.2",
40 | "step": "~0.0.5",
41 | "xml2js": "^0.4.23",
42 | "xml2js-xpath": "^0.11.0"
43 | },
44 | "scripts": {
45 | "lint:fix": "eslint --fix lib/ test/",
46 | "lint": "eslint lib/ test/",
47 | "pretest": "npm run lint",
48 | "test": "NODE_ENV=test mocha --exit -t 5000"
49 | },
50 | "engines": {
51 | "node": "^12.16.3",
52 | "npm": "^6.14.4"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/tools/reset_styles:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var path = require('path');
4 |
5 | // Reset all styles in the store
6 | var grainstore = require('../lib/grainstore');
7 |
8 | var redis = require('redis')
9 |
10 | function usage(me, exitcode) {
11 | console.log("Usage: " + me + " [--convert] ");
12 | process.exit(exitcode);
13 | }
14 |
15 | var doConvert = false;
16 | var MAPNIK_VERSION;
17 |
18 | var node_path = process.argv.shift();
19 | var script_path = process.argv.shift();
20 | var me = path.basename(script_path);
21 | var arg;
22 | while ( arg = process.argv.shift() ) {
23 | if ( arg == '--convert' ) {
24 | doConvert = true;
25 | } else if ( ! MAPNIK_VERSION ) {
26 | MAPNIK_VERSION = arg;
27 | }
28 | else {
29 | usage(me, 1);
30 | }
31 | }
32 |
33 | if ( ! MAPNIK_VERSION ) usage(me, 1);
34 |
35 | var REDIS_PORT = 6379; // TODO: make a command line parameter
36 |
37 | var dbnum = 0;
38 |
39 | var mml_store = new grainstore.MMLStore({port:REDIS_PORT}, {mapnik_version:MAPNIK_VERSION});
40 |
41 | var failures = [];
42 |
43 | var client = redis.createClient(REDIS_PORT, 'localhost');
44 | client.on('connect', function() {
45 | client.select(dbnum);
46 | client.keys('map_style|*', function(err, matches) {
47 |
48 | processNext = function() {
49 | if ( ! matches.length ) process.exit(failures.length);
50 | var k = matches.shift();
51 |
52 | if ( /map_style\|.*\|.*\|/.test(k) ) {
53 | //console.warn("Key " + k + " is EXTENDED, skipping");
54 | processNext();
55 | return;
56 | }
57 |
58 | var params = RegExp(/map_style\|(.*)\|(.*)/).exec(k);
59 | if ( ! params ) {
60 | console.warn("Key " + k + " is INVALID, skipping");
61 | processNext();
62 | return;
63 | }
64 | var db = params[1];
65 | var tab = params[2];
66 | var out = 'map_style|' + db + '|' + tab + ': ';
67 |
68 | var mml_builder = mml_store.mml_builder({dbname:db, table:tab},
69 | function(err, payload) {
70 |
71 | if ( err ) { console.warn(out + err.message); failures.push(k); processNext(); }
72 | else {
73 | mml_builder.resetStyle(function(err, data) {
74 | if ( err ) { console.warn(out + err.message); failures.push(k); }
75 | else console.log(out + 'OK' + ( doConvert ? ' (converted)' : '' ));
76 | processNext();
77 | }, doConvert);
78 | }
79 | });
80 |
81 | };
82 |
83 | processNext();
84 |
85 | });
86 |
87 | });
88 |
--------------------------------------------------------------------------------
/test/mml_builder_pool.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const assert = require('assert');
4 | const grainstore = require('../lib/grainstore');
5 | const DEFAULT_POINT_STYLE = `
6 | #layer {
7 | marker-fill: #FF6600;
8 | marker-opacity: 1;
9 | marker-width: 16;
10 | marker-line-color: white;
11 | marker-line-width: 3;
12 | marker-line-opacity: 0.9;
13 | marker-placement: point;
14 | marker-type: ellipse;
15 | marker-allow-overlap: true;
16 | }
17 | `;
18 | const SAMPLE_SQL = 'SELECT ST_MakePoint(0,0)';
19 |
20 | describe('mml_builder pool', function () {
21 | it('should fire timeout when "worker_timeout: 1"', function (done) {
22 | const mmlStore = new grainstore.MMLStore({ use_workers: true, worker_timeout: 1 });
23 | mmlStore
24 | .mml_builder({ dbname: 'my_database', sql: SAMPLE_SQL, style: DEFAULT_POINT_STYLE })
25 | .toXML((err) => {
26 | assert.equal(err.message, 'Timeout fired while generating Mapnik XML');
27 | done();
28 | });
29 | });
30 |
31 | it('should disable timeout when "worker_timeout: 0"', function (done) {
32 | const mmlStore = new grainstore.MMLStore({ use_workers: true, worker_timeout: 0 });
33 | mmlStore
34 | .mml_builder({ dbname: 'my_database', sql: SAMPLE_SQL, style: DEFAULT_POINT_STYLE })
35 | .toXML((err, xml) => {
36 | if (err) {
37 | return done(err);
38 | }
39 |
40 | assert.ok(xml.length > 0);
41 | return done();
42 | });
43 | });
44 |
45 | it('should NOT fire timeout when "worker_timeout: 2000"', function (done) {
46 | const mmlStore = new grainstore.MMLStore({ use_workers: true, worker_timeout: 2000 });
47 | mmlStore
48 | .mml_builder({ dbname: 'my_database', sql: SAMPLE_SQL, style: DEFAULT_POINT_STYLE })
49 | .toXML((err, xml) => {
50 | if (err) {
51 | return done(err);
52 | }
53 |
54 | assert.ok(xml.length > 0);
55 | return done();
56 | });
57 | });
58 |
59 | it('should NOT fire timeout when "worker_timeout: undefined"', function (done) {
60 | const mmlStore = new grainstore.MMLStore({ use_workers: true, worker_timeout: undefined });
61 | mmlStore
62 | .mml_builder({ dbname: 'my_database', sql: SAMPLE_SQL, style: DEFAULT_POINT_STYLE })
63 | .toXML((err, xml) => {
64 | if (err) {
65 | return done(err);
66 | }
67 |
68 | assert.ok(xml.length > 0);
69 | return done();
70 | });
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Grainstore
2 | ===========
3 |
4 | []
5 | (http://travis-ci.org/CartoDB/grainstore)
6 |
7 | Need to simply generate a Mapnik map from a dynamic PostGIS table?
8 |
9 | Grainstore is an opinionated [Carto](https://github.com/mapbox/carto)
10 | MML style store for PostGIS tables, views or sql queries that outputs
11 | Mapnik XML stylesheets.
12 |
13 | Map styles can be defined in the [Carto](https://github.com/mapbox/carto)
14 | map styling language or use default styles. The Carto styles are persisted
15 | and Mapnik XML output cached in Redis, making it a good choice for use
16 | in map tile servers.
17 |
18 | The generated Mapnik XML stylesheet plugs directly into Mapnik or Mapnik
19 | based tile server to render a map and interactivity layer.
20 |
21 | Grainstore is braindead simple:
22 |
23 | 1 db + 1 table/query + 1 style = 1 Mapnik XML stylesheet.
24 |
25 | or
26 |
27 | 1 db + N queries + N styles = 1 Mapnik XML stylesheet.
28 |
29 |
30 | Typical use
31 | -----------
32 | For using multiple layers use an array type for the 'sql' parameter and
33 | for the 'style' parameter. Each resulting layer will be named 'layerN'
34 | with N starting from 0 (needed to properly reference the layers from
35 | the 'style' values).
36 |
37 |
38 | Install
39 | --------
40 | npm install
41 |
42 |
43 | Dependencies
44 | ------------
45 | * node.js >=6
46 | * npm
47 |
48 |
49 | Additional test dependencies
50 | -----------------------------
51 | * libxml2
52 | * libxml2-devel
53 |
54 |
55 | Examples
56 | ---------
57 |
58 | ```javascript
59 |
60 | var grainstore = require('grainstore');
61 |
62 | var params = {
63 | dbname: 'my_database',
64 | sql:'select * from my_table',
65 | style: '#my_table { polygon-fill: #fff; }'
66 | }
67 |
68 | // fully default.
69 | var mmls = new grainstore.MMLStore();
70 | var mmlb = mmls.mml_builder(params);
71 | mmlb.toXML(function(err, data){
72 | console.log(data); // => Mapnik XML for your database with default styles
73 | });
74 |
75 |
76 | // custom pg settings.
77 | var mmls = new grainstore.MMLStore();
78 |
79 | // see mml_store.js for more customisation detail
80 | var options = {
81 | map: {srid: 4326},
82 | datasource: {
83 | user: "postgres",
84 | geometry_field: "my_geom"
85 | }
86 | }
87 |
88 | mmlb = mmls.mml_builder(params, options);
89 | mmlb.toXML(function(err, data){
90 | console.log(data); // => Mapnik XML of custom database with default style
91 | });
92 |
93 |
94 | ```
95 |
96 | For more examples, see the tests.
97 |
98 |
99 | Tests
100 | -----
101 | To run the tests, from the project root:
102 |
103 | ```
104 | npm test
105 | ```
106 |
107 |
108 | Release
109 | -------
110 |
111 | ```
112 | npm publish
113 | ```
114 |
115 | Contributing
116 | ------------
117 |
118 | See [CONTRIBUTING.md](CONTRIBUTING.md).
119 |
--------------------------------------------------------------------------------
/lib/grainstore/style_trans.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var debug = require('debug')('grainstore:style-trans');
4 |
5 | var semver = require('semver');
6 | var _ = require('underscore');
7 |
8 | var convert20To21 = require('./paths/from20To21');
9 | var convert23To30 = require('./paths/from23To30');
10 |
11 | function StyleTrans () {}
12 |
13 | module.exports = StyleTrans;
14 |
15 | var reMapnikGeometryType = new RegExp(/\bmapnik-geometry-type\b/g);
16 | function convertMapnikGeometryType (style) {
17 | return style.replace(reMapnikGeometryType, '"mapnik::geometry_type"');
18 | }
19 |
20 | function noop (style) { return style; }
21 |
22 | var tp = {};
23 | tp['2.0.0'] = {
24 | // NOTE: 2.0.1 intentionally left blank, no path to go there
25 | '2.0.2': noop,
26 | '2.0.3': noop,
27 | '2.0.4': noop,
28 | '2.1.0': convert20To21,
29 | '2.1.1': convert20To21
30 | };
31 | tp['2.0.2'] = tp['2.0.3'] = tp['2.0.4'] = tp['2.0.0'];
32 |
33 | tp['2.0.1'] = {
34 | // NOTE: not allowing path from 2.0.1 to ~2.0.2 as it would
35 | // require to half marker-width and marker-height
36 | '2.1.0': convert20To21
37 | };
38 |
39 | tp['2.1.0'] = {
40 | '2.1.1': noop,
41 | '2.2.0': noop
42 | };
43 | tp['2.1.1'] = tp['2.1.0'];
44 |
45 | tp['2.2.0'] = {
46 | '2.3.0': noop
47 | };
48 |
49 | tp['2.3.0'] = {
50 | '3.0.12': convert23To30
51 | };
52 |
53 | tp['3.0.12'] = {
54 | '3.0.15': noop
55 | };
56 |
57 | StyleTrans.prototype.setLayerName = function (css, layername) {
58 | var ret = css.replace(/#[^\s[{;:]+\s*([:\[{])/g, '#' + layername + ' $1'); // eslint-disable-line
59 | // console.log("PRE:"); console.log(css);
60 | // console.log("POS:"); console.log(ret);
61 | return ret;
62 | };
63 |
64 | // @param style CartoCSS
65 | // @param from source CartoCSS/Mapnik version
66 | // @param to target CartoCSS/Mapnik version
67 | StyleTrans.prototype.transform = function (style, from, to) {
68 | // For backward compatibility
69 | if (semver.satisfies(from, '<2.2.0')) {
70 | style = convertMapnikGeometryType(style);
71 | }
72 |
73 | while (from !== to) {
74 | var converter = null;
75 | var nextTarget = null;
76 | // 1. Find entry for 'from'
77 | if (Object.prototype.hasOwnProperty.call(tp, from)) {
78 | var ct = _.keys(tp[from]).sort(semver.compare);
79 | for (var i = ct.length; i; i--) {
80 | var t = ct[i - 1];
81 |
82 | if (semver.satisfies(t, '>' + to)) {
83 | continue;
84 | }
85 |
86 | converter = tp[from][t];
87 | nextTarget = t;
88 | break;
89 | }
90 | }
91 | if (!converter) {
92 | throw new Error('No CartoCSS transform path from ' + from + ' to ' + to);
93 | }
94 |
95 | try {
96 | style = converter(style, from, nextTarget);
97 | } catch (err) {
98 | debug('Error parsing style from %s to %s: %s', from, to, err);
99 | }
100 |
101 | from = nextTarget;
102 | }
103 |
104 | return style;
105 | };
106 |
--------------------------------------------------------------------------------
/lib/grainstore/mml_store.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var fs = require('fs');
4 | var _ = require('underscore');
5 | var debug = require('debug')('grainstore:mmlstore');
6 |
7 | var MMLBuilder = require('./mml-builder/mml-builder');
8 |
9 | // @param optional_args
10 | // optional configurations. valid elements:
11 | // cachedir: is base directory to put localized external resources into
12 | // Defaults to '/tmp/millstone'
13 | // *: anything else that is accepted by mml_builder "optional_args"
14 | // parameter, see mml_builder.js
15 | //
16 | //
17 | function MMLStore (options) {
18 | var me = {};
19 |
20 | options = options || {};
21 | options.cachedir = options.cachedir || '/tmp/millstone';
22 |
23 | // @param callback(err, payload) called on initialization
24 | me.mml_builder = function (params, overrideOptions) {
25 | var opts = _.extend({}, options, overrideOptions);
26 | return new MMLBuilder(params, opts);
27 | };
28 |
29 | /// API: Purge cache of localized resources for this store
30 | //
31 | /// @param ttl time to leave for each file, in seconds
32 | /// NOTE: you can use 0 to clean all resources
33 | ///
34 | /// @param lbl label prefix for logs
35 | ///
36 | me.purgeLocalizedResources = function (ttl, callback, lbl) {
37 | if (lbl) {
38 | lbl += ': ';
39 | } else {
40 | lbl = '';
41 | }
42 | var now = Date.now();
43 | // TODO: check if "base" should also be cleared
44 | var toclear = options.cachedir + '/cache';
45 | debug('Scanning cache dir %s', toclear);
46 | fs.readdir(toclear, function (err, files) {
47 | if (err) {
48 | if (err.code !== 'ENOENT') {
49 | callback(err);
50 | } else {
51 | callback(null);
52 | } // nothing to clear
53 | } else {
54 | var purgeNext = function () {
55 | var name = files.shift();
56 | if (!name) {
57 | callback(null); // end of files
58 | return;
59 | }
60 | var file = toclear + '/' + name;
61 | fs.stat(file, function (err, stats) {
62 | if (err) {
63 | debug('%scannot stat file %s: %s', lbl, file, err);
64 | purgeNext();
65 | }
66 | if (ttl) {
67 | var cage = (now - stats.ctime.getTime()) / 1000;
68 | var aage = (now - stats.atime.getTime()) / 1000;
69 | if (cage < ttl || aage < ttl) {
70 | purgeNext();
71 | return;
72 | }
73 | debug(
74 | '%sunlinking %s created %d seconds ago and accessed %d seconds ago (ttl is %d)',
75 | lbl, file, cage, aage, ttl
76 | );
77 | } else {
78 | debug('%sunlinking %s (ttl is %d)', lbl, file, ttl);
79 | }
80 | fs.unlink(file, function (err) {
81 | if (err) {
82 | debug('%serror unlinking %s: %s', lbl, file, err);
83 | }
84 | purgeNext();
85 | });
86 | });
87 | };
88 | purgeNext();
89 | }
90 | });
91 | };
92 |
93 | return me;
94 | }
95 |
96 | module.exports = MMLStore;
97 |
--------------------------------------------------------------------------------
/lib/grainstore/paths/from23To30.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var debug = require('debug')('grainstore:transform');
4 | var postcss = require('postcss');
5 | var syntax = require('postcss-scss');
6 | var stripInlineComments = require('postcss-strip-inline-comments');
7 |
8 | var propertyDefaultValues = [{
9 | symbolizer: /^polygon-(?!pattern-)/,
10 | defaults: [{
11 | property: 'polygon-clip',
12 | value: true
13 | }]
14 | }, {
15 | symbolizer: /^polygon-pattern-/,
16 | defaults: [{
17 | property: 'polygon-pattern-clip',
18 | value: true
19 | }, {
20 | property: 'polygon-pattern-alignment',
21 | value: 'local'
22 | }]
23 | }, {
24 | symbolizer: /^line-(?!pattern-)/,
25 | defaults: [{
26 | property: 'line-clip',
27 | value: true
28 | }]
29 | }, {
30 | symbolizer: /^line-pattern-/,
31 | defaults: [{
32 | property: 'line-pattern-clip',
33 | value: true
34 | }]
35 | }, {
36 | symbolizer: /^marker-/,
37 | defaults: [{
38 | property: 'marker-clip',
39 | value: true
40 | }]
41 | }, {
42 | symbolizer: /^marker-line-/,
43 | defaults: [{
44 | property: 'marker-line-width',
45 | value: 1
46 | }]
47 | }, {
48 | symbolizer: /^shield-/,
49 | defaults: [{
50 | property: 'shield-clip',
51 | value: true
52 | }]
53 | }, {
54 | symbolizer: /^text-/,
55 | defaults: [{
56 | property: 'text-clip',
57 | value: true
58 | }, {
59 | property: 'text-label-position-tolerance',
60 | value: 0
61 | }]
62 | }];
63 |
64 | module.exports = function (cartoCss) {
65 | return postcss()
66 | .use(stripInlineComments)
67 | .use(postcssCartocssMigrator())
68 | .process(cartoCss, { syntax: syntax })
69 | .css;
70 | };
71 |
72 | var postcssCartocssMigrator = postcss.plugin('postcss-cartocss-migrator', function (options) {
73 | options = options || {};
74 | return function (root) {
75 | root.walkRules(function (rule) {
76 | debug('Inspecting selector "%s"', rule.selector);
77 | propertyDefaultValues.forEach(function (property) {
78 | property.defaults.forEach(function (propertyDefault) {
79 | setPropertyToDefault(rule, property.symbolizer, propertyDefault.property, propertyDefault.value);
80 | });
81 | });
82 | });
83 | };
84 | });
85 |
86 | function setPropertyToDefault (rule, symbolizer, property, defaultValue) {
87 | if (hasDeclWithSymbolyzer(rule, symbolizer) && !hasPropertyDefined(rule, property)) {
88 | var declaration = postcss.decl({ prop: property, value: defaultValue });
89 | rule.append(declaration);
90 | debug('Appending declaration "%s: %s" to selector "%s"', property, defaultValue, rule.selector);
91 | }
92 | }
93 |
94 | function hasPropertyDefined (rule, property) {
95 | var hasProperty = false;
96 |
97 | if (rule.parent) {
98 | hasProperty = hasPropertyDefined(rule.parent, property);
99 | }
100 |
101 | if (!hasProperty) {
102 | rule.walkDecls(property, function (decl) {
103 | debug('Inspecting declaration "%s" to check if "%s" is already defined', decl, property);
104 | if (decl.parent === rule) {
105 | hasProperty = true;
106 | }
107 | });
108 | }
109 |
110 | return hasProperty;
111 | }
112 |
113 | function hasDeclWithSymbolyzer (rule, symbolyzer) {
114 | var hasPropertySymbolyzer = false;
115 |
116 | rule.walkDecls(symbolyzer, function (decl) {
117 | debug('Inspecting declaration "%s" to check if it has any property that belongs to "%s" symbolizer', decl, symbolyzer);
118 |
119 | if (decl.parent === rule) {
120 | hasPropertySymbolyzer = true;
121 | }
122 | });
123 |
124 | return hasPropertySymbolyzer;
125 | }
126 |
--------------------------------------------------------------------------------
/lib/grainstore/mml-builder/mml-builder.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var util = require('util');
4 | var MMLBuilderInline = require('./mml-builder-inline');
5 |
6 | var pool = require('generic-pool');
7 | var fork = require('child_process').fork;
8 | var debug = require('debug')('grainstore:mml-builder');
9 | var debugXml = require('debug')('grainstore:mml-builder:xml');
10 | var childMMLBuilderPool = null;
11 | var path = require('path');
12 |
13 | function createWorkersPool () {
14 | if (!childMMLBuilderPool) {
15 | childMMLBuilderPool = pool.Pool({
16 | name: 'mml-builder',
17 | create: function (callback) {
18 | try {
19 | const child = fork(path.join(__dirname, '/mml-builder-child.js'));
20 | return callback(null, child);
21 | } catch (err) {
22 | if (err instanceof Error && err.message.includes('ENOMEM')) {
23 | process.emit('ENOMEM');
24 | }
25 |
26 | return callback(err);
27 | }
28 | },
29 | destroy: function (child) {
30 | child.kill();
31 | },
32 | validate: function (child) {
33 | return child && child.connected;
34 | },
35 | max: 8,
36 | min: 2,
37 | idleTimeoutMillis: 60000,
38 | reapIntervalMillis: 5000,
39 | log: false
40 | });
41 |
42 | process.on('exit', function (code) {
43 | childMMLBuilderPool.destroyAllNow();
44 | });
45 | }
46 | }
47 |
48 | function MMLBuilder (params, options) {
49 | MMLBuilderInline.call(this, params, options);
50 |
51 | if (this.options.use_workers) {
52 | createWorkersPool();
53 | }
54 |
55 | this.options.worker_timeout = Number.isFinite(this.options.worker_timeout) ? this.options.worker_timeout : 5000;
56 | }
57 |
58 | util.inherits(MMLBuilder, MMLBuilderInline);
59 |
60 | module.exports = MMLBuilder;
61 |
62 | MMLBuilder.prototype.toXML = function (callback) {
63 | var self = this;
64 |
65 | if (!this.options.use_workers) {
66 | return MMLBuilderInline.prototype.toXML.call(this, callback);
67 | }
68 |
69 | debug('Acquiring child process');
70 | childMMLBuilderPool.acquire(function (err, child) {
71 | if (err) {
72 | return callback(new Error('Unable to generate Mapnik XML'));
73 | }
74 |
75 | debug('Waiting for Mapnik XML');
76 |
77 | function done (result) {
78 | debug('Child process sent a result back to the parent');
79 | if (timeout) {
80 | clearTimeout(timeout);
81 | }
82 |
83 | if (result.err && result.err.includes('ENOMEM')) {
84 | childMMLBuilderPool.destroy(child);
85 | process.emit('ENOMEM');
86 |
87 | return callback(new Error(result.err));
88 | }
89 |
90 | childMMLBuilderPool.release(child);
91 |
92 | if (result.err) {
93 | return callback(new Error(result.err));
94 | }
95 |
96 | if (!result.xml) {
97 | return callback(new Error('Unable to generate Mapnik XML'));
98 | }
99 |
100 | debug('Generated Mapnik XML');
101 | debugXml('output', result.xml);
102 |
103 | return callback(null, result.xml);
104 | }
105 |
106 | let timeout;
107 |
108 | if (self.options.worker_timeout > 0) {
109 | timeout = setTimeout(function () {
110 | child.removeListener('message', done);
111 | childMMLBuilderPool.destroy(child);
112 |
113 | debug('Timeout expired while generating Mapnik XML');
114 |
115 | return callback(new Error('Timeout fired while generating Mapnik XML'));
116 | }, self.options.worker_timeout);
117 | }
118 |
119 | child.once('message', done);
120 |
121 | debug('Sending Mapnik XML to child process');
122 |
123 | // Node converts a javascript object (w/ functions) to
124 | // a plain object (w/o functions) before sending it to child process,
125 | // thus we can use all bound context to MMLBuilder to clone it in child process
126 | return child.send({ context: self });
127 | });
128 | };
129 |
--------------------------------------------------------------------------------
/lib/grainstore/paths/from20To21.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _ = require('underscore');
4 | var semver = require('semver');
5 |
6 | module.exports = function convert20To21 (style, from, to) {
7 | // strip comments from styles
8 | // NOTE: these regexp will make a mess when comment-looking strings are put
9 | // in quoted strings. We take the risk, assuming it'd be very uncommon
10 | // to find literal in styles anyway...
11 | // console.log("X: " + style);
12 | style = style.replace(new RegExp('(/\\*[^\*]*\\*/)|(//.*\n)', 'g'), ''); // eslint-disable-line
13 | // console.log("Y: " + style);
14 |
15 | var globalMarkerDirectives = [];
16 | var globalHasMarkerDirectives = false;
17 | var re = new RegExp('([^{]*){([^}]*)}', 'g');
18 | var newstyle = style.replace(re, function (mtc, cond, stl) {
19 | // trim blank spaces (but not newlines) on both sides
20 | stl = stl.replace(/^[ \t]*/, '').replace(/[\t ]*$/, '');
21 | // add ending newline, if missing
22 | if (/[^\s]/.exec(stl) && !/;\s*$/.exec(stl)) stl += ';';
23 | var append = '';
24 |
25 | var isGlobalBlock = !/]\s*$/.exec(cond);
26 | var hasMarkerDirectives = globalHasMarkerDirectives;
27 | var re = new RegExp('(marker-[^\s:]*):\s*([^;}]*)', 'ig'); // eslint-disable-line
28 | var markerDirectives = isGlobalBlock ? globalMarkerDirectives : _.defaults([], globalMarkerDirectives);
29 | stl = stl.replace(re, function (m, l, v) {
30 | l = l.toLowerCase();
31 | if (!Object.prototype.hasOwnProperty.call(markerDirectives, l)) {
32 | markerDirectives[l] = v;
33 | }
34 | hasMarkerDirectives = true;
35 | if (isGlobalBlock) {
36 | globalHasMarkerDirectives = true;
37 | }
38 |
39 | // In mapnik-2.0.x, marker-opacity only set opacity of the marker
40 | // fill (but not stroke). This is equivalent to the mapnik-2.1.x
41 | // directive ``marker-fill-opacity``. We want to translate the
42 | // directive name beause ``marker-opacity`` also sets stroke
43 | // opacity with mapnik-2.1.x.
44 | //
45 | // See https://github.com/Vizzuality/grainstore/issues/40
46 | //
47 | m = m.replace(new RegExp('marker-opacity', 'i'), 'marker-fill-opacity');
48 |
49 | return m;
50 | });
51 |
52 | // Double marker-width and marker-height but not if source is '2.0.1'
53 | // TODO: put within hasMarkerDirectives
54 | if (from !== '2.0.1') {
55 | var reDouble = new RegExp('marker-(width|height)[\t\n ]*:[\t\n ]*(["\']?)([^\'";}]*)["\']?\\b', 'g'); // eslint-disable-line
56 | stl = stl.replace(reDouble, function (m, l, q, v) {
57 | return 'marker-' + l + ':' + q + (v * 2);
58 | });
59 | }
60 |
61 | // console.log("Has marker directives: " + hasMarkerDirectives );
62 |
63 | // Set marker-related defaults but only if any
64 | // "marker-xxx" directive is given
65 | if (hasMarkerDirectives) {
66 | // For points, set:
67 | // marker-placement:point (in 2.1.0 marker-placement:line doesn't work with points)
68 | // marker-type:ellipse (in 2.0.0 marker-type:arrow didn't work with points)
69 | append += ' ["mapnik::geometry_type"=1] { marker-placement:point; marker-type:ellipse; }';
70 |
71 | var linePolyOverride = ' ["mapnik::geometry_type">1] { ';
72 |
73 | // Set marker-placement:line for lines and polys
74 | // but only if a marker-placement isn't already present
75 | if (!markerDirectives['marker-placement']) {
76 | linePolyOverride += 'marker-placement:line; ';
77 | }
78 |
79 | var hasArrowMarker = (markerDirectives.marker_type === 'arrow');
80 |
81 | // Set to marker-type:arrow for lines and polys
82 | // but only if a marker-type isn't already present and
83 | // the marker-placement directive requests a point (didn't work in 2.0)
84 | if (!markerDirectives['marker-type'] && markerDirectives['marker-placement'] !== 'point') {
85 | linePolyOverride += 'marker-type:arrow; ';
86 | hasArrowMarker = true;
87 | }
88 |
89 | // See https://github.com/mapnik/mapnik/issues/1591#issuecomment-10740221
90 | if (hasArrowMarker) {
91 | linePolyOverride += 'marker-transform:scale(.5, .5); ';
92 | }
93 |
94 | // If the marker-placement directive requested a point we'll use ellipse marker-type
95 | // as 2.0 didn't work with arrows and points..
96 | if (markerDirectives['marker-placement'] === 'point') {
97 | linePolyOverride += 'marker-type:ellipse; ';
98 | }
99 |
100 | // 2.0.0 did not clip geometries before sending
101 | // to style renderer
102 | linePolyOverride += 'marker-clip:false; ';
103 |
104 | linePolyOverride += '}';
105 |
106 | append += linePolyOverride;
107 |
108 | if (semver.satisfies(to, '~2.1.1')) {
109 | // See https://github.com/Vizzuality/grainstore/issues/36
110 | append += ' marker-multi-policy:whole;';
111 | }
112 | }
113 |
114 | // console.log("STYLE: [" + style + "]");
115 | // console.log(" STL: [" + stl + "]");
116 |
117 | var newblock = cond + '{ ' + stl + append + ' }';
118 |
119 | return newblock;
120 | });
121 |
122 | // console.log("PRE:"); console.log(style);
123 | style = newstyle;
124 | // console.log("POS:"); console.log(style);
125 |
126 | return style;
127 | };
128 |
--------------------------------------------------------------------------------
/lib/grainstore/mml-builder/mml-builder-inline.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _ = require('underscore');
4 | var millstone = require('millstone');
5 | var StyleTrans = require('../style_trans');
6 | var carto = require('carto');
7 | var debug = require('debug')('grainstore:mml-builder-inline');
8 | var debugXml = require('debug')('grainstore:mml-builder-inline:xml');
9 |
10 | // configure grainstore from optional args passed + defaults
11 | var grainstoreDefaults = {
12 | map: {
13 | srid: 3857,
14 | 'maximum-extent': '-20037508.3,-20037508.3,20037508.3,20037508.3'
15 | },
16 | datasource: {
17 | type: 'postgis',
18 | // host: "localhost", // let default be driven by env/lib
19 | // user: "postgres", // let default be driven by env/lib
20 | geometry_field: 'the_geom_webmercator',
21 | extent: '-20037508.3,-20037508.3,20037508.3,20037508.3',
22 | srid: 3857,
23 | max_size: 10
24 | }
25 | };
26 |
27 | var supportedDatasourceTypes = {
28 | geometry: {
29 | type: 'postgis',
30 | prop: 'geometry_field'
31 | },
32 | raster: {
33 | type: 'pgraster',
34 | prop: 'raster_field'
35 | }
36 | };
37 |
38 | // MML builder interface
39 | //
40 | // @param params
41 | // params must have:
42 | // `dbname` - name of database
43 | //
44 | // params may have:
45 | // `sql` - sql to constrain the map by (can be an array)
46 | // `ids` - optional array of layer identifiers to override the default layer id/name: `layer{index}`
47 | // `gcols` - optional array of geometry column string names (defaulting to type 'geometry')
48 | // or objects with {type: 'column_type', name: 'column_name'} where:
49 | // - name is mandatory
50 | // - type is optional, accepted values ['geometry', 'raster'] (default: 'geometry')
51 | // `extra_ds_opts` - optional array of extra datasource options (will only be used when missing from global datasource)
52 | // `datasource_extend` - optional array of extra datasource options (will override the global datasource)
53 | // `geom_type` - [polygon|point] to specify which default style to use
54 | // `style` - Carto style to override the built in style store (can be an array)
55 | // `style_version` - Version of the carto style override (can be an array)
56 | // `interactivity` - Comma separated list of grid fields (can be an array)
57 | // `layer` - Interactivity layer index, to use with token and grids
58 | // `dbuser` - Database username
59 | // `dbpassword` - Database password
60 | // `dbhost` - Database host
61 | // `dbport` - Database port
62 | //
63 | // @param options
64 | // You may pass in a second argument to override grainstore defaults.
65 | // `map` specifies the output map projection.
66 | // `datasource` specifies postgis details from Mapnik postgis plugin:
67 | // https://github.com/mapnik/mapnik/wiki
68 | // `cachedir` is base directory to put localized external resources into
69 | // `carto_env` carto renderer environment options, see
70 | // http://github.com/mapbox/carto/blob/v0.9.5/lib/carto/renderer.js#L71
71 | // `mapnik_version` is target version of mapnik, defaults to ``2.0.2``
72 | // `mapnik_tile_format` for the tiles, see https://github.com/mapnik/mapnik/wiki/OutputFormats
73 | // `default_style_version` is the default version for CartoCSS styles. Defaults to '2.0.0'
74 | //
75 | // eg.
76 | // {
77 | // map: {srid: 3857},
78 | // datasource: {
79 | // type: "postgis",
80 | // host: "localhost",
81 | // user: "postgres",
82 | // geometry_field: "the_geom_webmercator",
83 | // extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
84 | // srid: 3857,
85 | // max_size: 10
86 | // }
87 | // }
88 | //
89 | function MMLBuilderInline (params, options) {
90 | this.params = params || {};
91 | // core variables
92 | var requiredParams = ['dbname', 'sql'];
93 |
94 | requiredParams.forEach(function (paramKey) {
95 | if (!Object.prototype.hasOwnProperty.call(params, paramKey)) {
96 | throw new Error("Options must include '" + paramKey + "'");
97 | }
98 | });
99 |
100 | this.options = options || {};
101 | this.options.cachedir = this.options.cachedir || '/tmp/millstone';
102 | this.target_mapnik_version = options.mapnik_version || '2.0.2';
103 | this.default_style_version = options.default_style_version || '2.0.0';
104 |
105 | this.grainstore_datasource = _.defaults(_.clone(options.datasource || {}), grainstoreDefaults.datasource);
106 |
107 | // Allow overriding db authentication with options
108 | if (params.dbuser) {
109 | this.grainstore_datasource.user = params.dbuser;
110 | }
111 | if (params.dbpassword) {
112 | this.grainstore_datasource.password = params.dbpassword;
113 | }
114 | if (params.dbhost) {
115 | this.grainstore_datasource.host = params.dbhost;
116 | }
117 | if (params.dbport) {
118 | this.grainstore_datasource.port = params.dbport;
119 | }
120 |
121 | this.grainstore_map = _.defaults(options.map || {}, grainstoreDefaults.map);
122 | if (options.mapnik_tile_format) {
123 | this.grainstore_map.format = options.mapnik_tile_format;
124 | }
125 | if (params.markers_symbolizer_caches) {
126 | this.grainstore_map.markers_symbolizer_caches = params.markers_symbolizer_caches;
127 | }
128 |
129 | this.interactivity = params.interactivity;
130 | if (_.isString(this.interactivity)) {
131 | this.interactivity = [this.interactivity];
132 | } else if (this.interactivity) {
133 | for (var i = 0; i < this.interactivity.length; ++i) {
134 | if (this.interactivity[i] && !_.isString(this.interactivity[i])) {
135 | throw new Error('Invalid interactivity value type for layer ' + i + ': ' + typeof (this.interactivity[i]));
136 | }
137 | }
138 | }
139 | this.interactivity_layer = params.layer || 0;
140 | if (!Number.isFinite(this.interactivity_layer)) {
141 | throw new Error('Invalid (non-integer) layer value type: ' + this.interactivity_layer);
142 | }
143 | }
144 |
145 | module.exports = MMLBuilderInline;
146 |
147 | MMLBuilderInline.prototype.set = function (property, value) {
148 | var isOption = Object.prototype.hasOwnProperty.call(this, property) && !_.isFunction(this[property]);
149 |
150 | if (!isOption) {
151 | throw new Error('Setting "' + property + '" is not allowed');
152 | }
153 |
154 | this[property] = _.extend(this[property], value);
155 |
156 | return this; // allow chaining
157 | };
158 |
159 | MMLBuilderInline.prototype.toXML = function (callback) {
160 | var style = this.params.style;
161 | var styleVersion = this.params.style_version || this.default_style_version;
162 |
163 | this.render(style, styleVersion, callback);
164 | };
165 |
166 | MMLBuilderInline.prototype.render = function (styleIn, version, callback) {
167 | const self = this;
168 | let style = '';
169 | let mml;
170 |
171 | try {
172 | if (styleIn) {
173 | style = this.transformStyle(styleIn, version);
174 | }
175 | mml = this.toMML(style);
176 | } catch (err) {
177 | return callback(err);
178 | }
179 |
180 | // Millstone configuration
181 | //
182 | // Resources are shared between all maps, and ensured
183 | // to be localized on every call to the "toXML" method.
184 | //
185 | // Caller should take care of purging unused resources based
186 | // on its usage of the "toXML" method.
187 | //
188 | var millstoneOptions = {
189 | mml: mml,
190 | base: this.options.cachedir + '/base',
191 | cache: this.options.cachedir + '/cache'
192 | };
193 |
194 | millstone.resolve(millstoneOptions, function renderResolvedMml (err, mml) {
195 | if (err) {
196 | return callback(err, null);
197 | }
198 |
199 | // NOTE: we _need_ a new object here because carto writes into it
200 | var cartoEnv = _.defaults({}, self.options.carto_env);
201 | var cartoOptions = { mapnik_version: self.target_mapnik_version };
202 |
203 | // carto.Renderer may throw during parse time (before nextTick is called)
204 | // See https://github.com/mapbox/carto/pull/187
205 | try {
206 | var r = new carto.Renderer(cartoEnv, cartoOptions);
207 | debug('Renderer.render start');
208 |
209 | var xml = r.render(mml);
210 | debugXml('output', xml);
211 |
212 | process.nextTick(function () {
213 | return callback(null, xml);
214 | });
215 | } catch (err) {
216 | return callback(err, null);
217 | }
218 | });
219 | };
220 |
221 | MMLBuilderInline.prototype.transformStyle = function (styleIn, version) {
222 | styleIn = Array.isArray(styleIn) ? styleIn : [styleIn];
223 |
224 | var style = [];
225 |
226 | var styleTransformer = new StyleTrans();
227 |
228 | for (var i = 0; i < styleIn.length; ++i) {
229 | if (styleIn[i].replace(/^\s+|\s+$/g, '').length === 0) {
230 | throw new Error('style' + i + ': CartoCSS is empty');
231 | }
232 |
233 | var v = _.isArray(version) ? version[i] : version;
234 | if (!v) {
235 | v = this.default_style_version;
236 | }
237 | style[i] = styleTransformer.transform(styleIn[i], v, this.target_mapnik_version);
238 | }
239 |
240 | return style;
241 | };
242 |
243 | MMLBuilderInline.prototype.baseMML = function () {
244 | var tables = Array.isArray(this.params.sql) ? this.params.sql : [this.params.sql];
245 |
246 | var mml = {};
247 | mml.srs = '+init=epsg:' + this.grainstore_map.srid;
248 | mml['maximum-extent'] = this.grainstore_map['maximum-extent'];
249 | mml.format = this.grainstore_map.format || 'png';
250 | mml.Layer = [];
251 | if (this.grainstore_map.markers_symbolizer_caches) {
252 | mml.markers_symbolizer_caches_disabled = this.grainstore_map.markers_symbolizer_caches.disabled.toString();
253 | }
254 |
255 | for (var i = 0; i < tables.length; ++i) {
256 | var table = tables[i];
257 |
258 | var datasource = _.clone(this.grainstore_datasource);
259 | datasource.table = table;
260 | datasource.dbname = this.params.dbname;
261 | if (this.params.search_path) {
262 | datasource.search_path = this.params.search_path;
263 | }
264 |
265 | if (this.params.gcols && this.params.gcols[i]) {
266 | if (_.isString(this.params.gcols[i])) {
267 | datasource.geometry_field = this.params.gcols[i];
268 | } else {
269 | var gcol = this.params.gcols[i];
270 | gcol.type = gcol.type || 'geometry';
271 | var dsOpts = supportedDatasourceTypes[gcol.type];
272 | if (dsOpts && gcol.name) {
273 | delete datasource.geometry_field;
274 | datasource.type = dsOpts.type;
275 | datasource[dsOpts.prop] = gcol.name;
276 | } else {
277 | throw new Error('Unsupported geometry column type for layer ' + i + ': ' + gcol.type);
278 | }
279 | }
280 | }
281 |
282 | if (this.params.datasource_extend && this.params.datasource_extend[i]) {
283 | datasource = _.extend(datasource, this.params.datasource_extend[i]);
284 | }
285 |
286 | if (this.params.extra_ds_opts) {
287 | datasource = _.defaults(datasource, this.params.extra_ds_opts[i]);
288 | }
289 |
290 | var layer = {};
291 | layer.id = getLayerName(this.params, i);
292 | layer.name = layer.id;
293 | layer.srs = '+init=epsg:' + datasource.srid;
294 | layer.properties = getLayerProperties(this.params, i);
295 | layer.Datasource = datasource;
296 |
297 | mml.Layer.push(layer);
298 | }
299 |
300 | if (this.interactivity) {
301 | if (this.interactivity[this.interactivity_layer]) {
302 | if (_.isString(this.interactivity[this.interactivity_layer])) {
303 | mml.interactivity = {
304 | layer: mml.Layer[this.interactivity_layer].id,
305 | fields: this.interactivity[this.interactivity_layer].split(',')
306 | };
307 | } else {
308 | throw new Error('Unexpected interactivity format: ' + this.interactivity[this.interactivity_layer]);
309 | }
310 | }
311 | }
312 |
313 | return mml;
314 | };
315 |
316 | MMLBuilderInline.prototype.toMML = function (styleIn) {
317 | var baseMML = this.baseMML();
318 | baseMML.Stylesheet = [];
319 |
320 | var style = Array.isArray(styleIn) ? styleIn : [styleIn];
321 | var t = new StyleTrans();
322 |
323 | for (var i = 0; i < style.length; ++i) {
324 | var stylesheet = {};
325 | if (_.isArray(styleIn)) {
326 | stylesheet.id = 'style' + i;
327 | stylesheet.data = t.setLayerName(style[i], getLayerName(this.params, i));
328 | } else {
329 | stylesheet.id = 'style.mss';
330 | stylesheet.data = style[i];
331 | }
332 | baseMML.Stylesheet.push(stylesheet);
333 | }
334 |
335 | return baseMML;
336 | };
337 |
338 | function getLayerName (params, i) {
339 | var layerName = 'layer' + i;
340 |
341 | if (params.ids && !!params.ids[i]) {
342 | layerName = params.ids[i];
343 | }
344 |
345 | return layerName;
346 | }
347 |
348 | var ZOOM_PROPS = ['minzoom', 'maxzoom'];
349 | const SUPPORTED_PROPS = {
350 | // minzoom and maxzoom are included in zoom
351 | 'minimum-scale-denominator': true, /* double */
352 | 'maximum-scale-denominator': true, /* double */
353 | queryable: true, /* boolean */
354 | 'clear-label-cache"': true, /* boolean */
355 | 'cache-features': true, /* boolean */
356 | 'group-by"': true, /* string */
357 | 'buffer-size': true, /* int */
358 | 'maximum-extent': true /* string */
359 | };
360 | function getLayerProperties (params, i) {
361 | var properties = {};
362 |
363 | var zooms = params.zooms || [];
364 | var zoom = zooms[i] || {};
365 | ZOOM_PROPS.forEach(function (prop) {
366 | if (Object.prototype.hasOwnProperty.call(zoom, prop) && Number.isFinite(zoom[prop])) {
367 | properties[prop] = zoom[prop];
368 | }
369 | });
370 |
371 | Object.keys(params).forEach(function (key, index) {
372 | if (Object.prototype.hasOwnProperty.call(SUPPORTED_PROPS, key)) {
373 | if (Array.isArray(params[key])) {
374 | properties[key] = params[key][i];
375 | } else {
376 | properties[key] = params[key];
377 | }
378 | }
379 | });
380 |
381 | return properties;
382 | }
383 |
--------------------------------------------------------------------------------
/test/style_trans.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var assert = require('assert');
4 | var StyleTrans = require('../lib/grainstore/style_trans.js');
5 |
6 | var t = new StyleTrans();
7 |
8 | describe('style_trans', function () {
9 | // before(function() { });
10 |
11 | // No change from 2.0.0 to ~2.0.2
12 | it('2.0.0 to ~2.0.2', function () {
13 | var style = "#tab[zoom=1] { marker-width:10; marker-height:20; }\n#tab[zoom=2] { marker-height:'6'; marker-width: '7'; }";
14 | var s = t.transform(style, '2.0.0', '2.0.2');
15 | assert.equal(s, style);
16 | s = t.transform(style, '2.0.0', '2.0.3');
17 | assert.equal(s, style);
18 | });
19 |
20 | // No change from 2.0.2 to ~2.0.3
21 | it('2.0.2 to ~2.0.3', function () {
22 | var style = '#tab[zoom=1] { marker-width:10; marker-height:20; }\n' +
23 | "#tab[zoom=2] { marker-height:'6'; marker-width: '7'; }";
24 | var s = t.transform(style, '2.0.2', '2.0.3');
25 | assert.equal(s, style);
26 | });
27 |
28 | // Adapts marker width and height, from 2.0.2 to 2.1.0
29 | it('2.0.2 to 2.1.0, markers', function () {
30 | var s = t.transform(
31 | "#tab[zoom=1] { marker-width:10; marker-height:20; }\n#tab[zoom=2] { marker-height:'6'; marker-width: '7'; }",
32 | '2.0.2', '2.1.0'
33 | );
34 | var e = "#tab[zoom=1] { marker-width:20; marker-height:40; [\"mapnik::geometry_type\"=1] { marker-placement:point; marker-type:ellipse; } [\"mapnik::geometry_type\">1] { marker-placement:line; marker-type:arrow; marker-transform:scale(.5, .5); marker-clip:false; } }\n#tab[zoom=2] { marker-height:'12'; marker-width:'14'; [\"mapnik::geometry_type\"=1] { marker-placement:point; marker-type:ellipse; } [\"mapnik::geometry_type\">1] { marker-placement:line; marker-type:arrow; marker-transform:scale(.5, .5); marker-clip:false; } }";
35 | assert.equal(s, e);
36 |
37 | s = t.transform(
38 | '#t { marker-width :\n10; \nmarker-height\t: 20; }'
39 | , '2.0.2', '2.1.0'
40 | );
41 | e = '#t { marker-width:20; \n' +
42 | 'marker-height:40; ["mapnik::geometry_type"=1] { marker-placement:point; marker-type:ellipse; } ' +
43 | '["mapnik::geometry_type">1] { ' +
44 | 'marker-placement:line; marker-type:arrow; marker-transform:scale(.5, .5); marker-clip:false; } }';
45 | assert.equal(s, e);
46 | });
47 |
48 | // More markers, see https://github.com/Vizzuality/grainstore/issues/30
49 | it('2.0.0 to 2.1.0, more markers', function () {
50 | var s = t.transform('#t [a<1] { marker-width:1 } # [a>1] { marker-width:2 }', '2.0.2', '2.1.0');
51 | var e = '#t [a<1] { marker-width:2; ' +
52 | '["mapnik::geometry_type"=1] { marker-placement:point; marker-type:ellipse; } ' +
53 | '["mapnik::geometry_type">1] {' +
54 | ' marker-placement:line; marker-type:arrow; marker-transform:scale(.5, .5); marker-clip:false; } }' +
55 | ' # [a>1] { marker-width:4; ' +
56 | '["mapnik::geometry_type"=1] { marker-placement:point; marker-type:ellipse; } ' +
57 | '["mapnik::geometry_type">1] {' +
58 | ' marker-placement:line; marker-type:arrow; marker-transform:scale(.5, .5); marker-clip:false; } }';
59 | // console.log("O:"+s);
60 | // console.log("E:"+e);
61 | assert.equal(s, e);
62 | });
63 |
64 | // More markers, see https://github.com/Vizzuality/grainstore/issues/33
65 | it('2.0.0 to 2.1.0, markers dependent on filter', function () {
66 | var s = t.transform(
67 | '#t[a=1] { marker-width:1 } #t[a=2] { line-color:red } #t[a=3] { marker-placement:line }', '2.0.2', '2.1.0'
68 | );
69 | var e = '#t[a=1] { marker-width:2; ' +
70 | '["mapnik::geometry_type"=1] { marker-placement:point; marker-type:ellipse; } ' +
71 | '["mapnik::geometry_type">1] { ' +
72 | 'marker-placement:line; marker-type:arrow; marker-transform:scale(.5, .5); marker-clip:false; } } ' +
73 | '#t[a=2] { line-color:red; } ' +
74 | '#t[a=3] { marker-placement:line; ' +
75 | // NOTE: we do override marker-placement for points because "line" doesn't work in 2.1.0
76 | // and it worked exactly as "point" in 2.0.0
77 | '["mapnik::geometry_type"=1] { marker-placement:point; marker-type:ellipse; } ' +
78 | // NOTE: we do NOT override marker-placement for lines or polys
79 | '["mapnik::geometry_type">1] { marker-type:arrow; marker-transform:scale(.5, .5); marker-clip:false; } }';
80 | assert.equal(s, e);
81 | });
82 |
83 | // Adapts marker width and height, from 2.0.0 to 2.1.0
84 | it('2.0.0 to 2.1.0, markers', function () {
85 | var s = t.transform(
86 | "#tab[zoom=1] { marker-width:10; marker-height:20; }\n#tab[zoom=2] { marker-height:'6'; marker-width: \"7\"; }",
87 | '2.0.0', '2.1.0'
88 | );
89 | var e = "#tab[zoom=1] { marker-width:20; marker-height:40; [\"mapnik::geometry_type\"=1] { marker-placement:point; marker-type:ellipse; } [\"mapnik::geometry_type\">1] { marker-placement:line; marker-type:arrow; marker-transform:scale(.5, .5); marker-clip:false; } }\n#tab[zoom=2] { marker-height:'12'; marker-width:\"14\"; [\"mapnik::geometry_type\"=1] { marker-placement:point; marker-type:ellipse; } [\"mapnik::geometry_type\">1] { marker-placement:line; marker-type:arrow; marker-transform:scale(.5, .5); marker-clip:false; } }";
90 | // console.log("O:"+s);
91 | // console.log("E:"+e);
92 | assert.equal(s, e, 'Obt:' + s + '\nExp:' + s);
93 |
94 | s = t.transform(
95 | '#t { marker-width :\n10; \nmarker-height\t: 20; }'
96 | , '2.0.0', '2.1.0'
97 | );
98 | e = '#t { marker-width:20; \nmarker-height:40; ["mapnik::geometry_type"=1] { marker-placement:point; marker-type:ellipse; } ["mapnik::geometry_type">1] { marker-placement:line; marker-type:arrow; marker-transform:scale(.5, .5); marker-clip:false; } }';
99 | assert.equal(s, e);
100 |
101 | s = t.transform(
102 | '#tab { marker-width:2 }'
103 | , '2.0.0', '2.1.0'
104 | );
105 | e = '#tab { marker-width:4; ["mapnik::geometry_type"=1] { marker-placement:point; marker-type:ellipse; } ["mapnik::geometry_type">1] { marker-placement:line; marker-type:arrow; marker-transform:scale(.5, .5); marker-clip:false; } }';
106 | assert.equal(s, e);
107 |
108 | s = t.transform(
109 | '#tab{ marker-width:2 }'
110 | , '2.0.0', '2.1.0'
111 | );
112 | e = '#tab{ marker-width:4; ["mapnik::geometry_type"=1] { marker-placement:point; marker-type:ellipse; } ["mapnik::geometry_type">1] { marker-placement:line; marker-type:arrow; marker-transform:scale(.5, .5); marker-clip:false; } }';
113 | assert.equal(s, e);
114 | });
115 |
116 | it('2.0.0 to 2.1.0, line clipping', function () {
117 | var s = t.transform(
118 | '#tab{ line-opacity:.5 }'
119 | , '2.0.0', '2.1.0'
120 | );
121 | var e = '#tab{ line-opacity:.5; }';
122 | assert.equal(s, e);
123 | });
124 |
125 | it('2.0.0 to 2.1.0, line clipping, bug #37', function () {
126 | var s = t.transform(
127 | '#t{ marker-line-color:red; }'
128 | , '2.0.0', '2.1.0'
129 | );
130 | var e = '#t{ marker-line-color:red; ["mapnik::geometry_type"=1] { marker-placement:point; marker-type:ellipse; } ["mapnik::geometry_type">1] { marker-placement:line; marker-type:arrow; marker-transform:scale(.5, .5); marker-clip:false; } }';
131 | assert.equal(s, e);
132 | });
133 |
134 | it('2.0.0 to 2.1.0, polygon clipping', function () {
135 | var s = t.transform(
136 | '#tab{ polygon-fill:red }'
137 | , '2.0.0', '2.1.0'
138 | );
139 | var e = '#tab{ polygon-fill:red; }';
140 | assert.equal(s, e);
141 | });
142 |
143 | // https://github.com/Vizzuality/grainstore/issues/35
144 | it('2.0.0 to 2.1.0, one line comments', function () {
145 | var s = t.transform(
146 | '#tab{ //polygon-fill:red;\n}'
147 | , '2.0.0', '2.1.0'
148 | );
149 | var e = '#tab{ }';
150 | assert.equal(s, e);
151 | });
152 |
153 | // https://github.com/Vizzuality/grainstore/issues/41
154 | it('2.0.0 to 2.1.0, multiple one line comments', function () {
155 | var s = t.transform(
156 | '#tab{ //polygon-fill:red;\n //marker-type:ellipse;\n }'
157 | , '2.0.0', '2.1.0'
158 | );
159 | var e = '#tab{ }';
160 | assert.equal(s, e);
161 | });
162 |
163 | it('2.0.0 to 2.1.0, symbolizers hidden in one line comments', function () {
164 | var s = t.transform(
165 | '#tab{ //polygon-fill:red;\n line-opacity:1; }'
166 | , '2.0.0', '2.1.0'
167 | );
168 | var e = '#tab{ line-opacity:1; }';
169 | assert.equal(s, e);
170 | });
171 |
172 | it('2.0.0 to 2.1.0, symbolizers hidden in multiline line comments', function () {
173 | var s = t.transform(
174 | '#tab{ /* polygon-fill:\nred; */ line-opacity:1; }'
175 | , '2.0.0', '2.1.0'
176 | );
177 | var e = '#tab{ line-opacity:1; }';
178 | assert.equal(s, e);
179 | });
180 |
181 | it('2.0.0 to 2.1.0, missing semicolon', function () {
182 | var s = t.transform(
183 | '#t{ marker-placement:point; marker-width:8}'
184 | , '2.0.0', '2.1.0'
185 | );
186 | var e = '#t{ marker-placement:point; marker-width:16; ["mapnik::geometry_type"=1] { marker-placement:point; marker-type:ellipse; } ["mapnik::geometry_type">1] { marker-type:ellipse; marker-clip:false; } }';
187 | assert.equal(s, e);
188 | });
189 |
190 | it('2.0.0 to 2.1.1, marker-multi-policy', function () {
191 | var s = t.transform(
192 | '#t{ marker-fill-color:red; }'
193 | , '2.0.0', '2.1.1'
194 | );
195 | var e = '#t{ marker-fill-color:red; ["mapnik::geometry_type"=1] { marker-placement:point; marker-type:ellipse; } ["mapnik::geometry_type">1] { marker-placement:line; marker-type:arrow; marker-transform:scale(.5, .5); marker-clip:false; } marker-multi-policy:whole; }';
196 | assert.equal(s, e);
197 | });
198 |
199 | // Nothing to adapt (yet) when no markers are involved
200 | it('2.0.0 to 2.1.0, no markers', function () {
201 | var s = t.transform(
202 | '#tab[zoom=1] { line-fill:red; }\n#tab[zoom=2] { polygon-fill:blue; }'
203 | , '2.0.0', '2.1.0'
204 | );
205 | assert.equal(s,
206 | '#tab[zoom=1] { line-fill:red; }\n#tab[zoom=2] { polygon-fill:blue; }'
207 | );
208 | });
209 |
210 | // See https://github.com/Vizzuality/grainstore/issues/39
211 | it('2.0.0 to 2.1.0, arrow marker specified in outer block', function () {
212 | var s = t.transform(
213 | '#t{ marker-type:arrow; } #t[id=1] { marker-fill:red; }'
214 | , '2.0.0', '2.1.0'
215 | );
216 | var e = '#t{ marker-type:arrow; ["mapnik::geometry_type"=1] { marker-placement:point; marker-type:ellipse; } ["mapnik::geometry_type">1] { marker-placement:line; marker-clip:false; } } #t[id=1] { marker-fill:red; ["mapnik::geometry_type"=1] { marker-placement:point; marker-type:ellipse; } ["mapnik::geometry_type">1] { marker-placement:line; marker-clip:false; } }';
217 | assert.equal(s, e);
218 | });
219 |
220 | // See https://github.com/Vizzuality/grainstore/issues/40
221 | it('2.0.0 to 2.1.0, marker-opacity', function () {
222 | var s = t.transform(
223 | '#t{marker-opacity:0.5;}'
224 | , '2.0.0', '2.1.0'
225 | );
226 | var e = '#t{ marker-fill-opacity:0.5; ["mapnik::geometry_type"=1] { marker-placement:point; marker-type:ellipse; } ["mapnik::geometry_type">1] { marker-placement:line; marker-type:arrow; marker-transform:scale(.5, .5); marker-clip:false; } }';
227 | assert.equal(s, e);
228 | });
229 |
230 | it('transform retains quotes in CartoCSS', function () {
231 | var s = t.transform("#t [t=\"ja'ja\\\"ja\"] { }", '2.0.0', '2.1.0');
232 | var e = "#t [t=\"ja'ja\\\"ja\"] { }";
233 | assert.equal(s, e);
234 |
235 | s = t.transform("#t [t='ja\\'ja\"ja'] { }", '2.0.0', '2.1.0');
236 | e = "#t [t='ja\\'ja\"ja'] { }";
237 | assert.equal(s, e);
238 | });
239 |
240 | it('2.1.1 to 2.2.0, mapnik-geometry-type', function () {
241 | var s = t.transform(
242 | '#t [mapnik-geometry-type=1] { marker-fill:red; }'
243 | , '2.1.1', '2.2.0'
244 | );
245 | var e = '#t ["mapnik::geometry_type"=1] { marker-fill:red; }';
246 | assert.equal(s, e);
247 | });
248 |
249 | it('2.0.1 to 2.2.0', function () {
250 | var s = t.transform(
251 | '#t [mapnik-geometry-type=1] { line-color:red; }'
252 | , '2.0.1', '2.2.0'
253 | );
254 | var e = '#t ["mapnik::geometry_type"=1] { line-color:red; }';
255 | assert.equal(s, e);
256 | });
257 |
258 | it('2.1.1 to 2.1.0', function () {
259 | var e = null;
260 | try { t.transform('#t { }', '2.1.1', '2.1.0'); } catch (err) { e = err; }
261 | assert.ok(e);
262 | assert.ok(RegExp(/No CartoCSS transform path/).exec(e),
263 | 'Unexpected exception message ' + e);
264 | });
265 |
266 | it('2.1.1 to 2.2.1', function () {
267 | var e = null;
268 | try { t.transform('#t { }', '2.1.1', '2.2.1'); } catch (err) { e = err; }
269 | assert.ok(e);
270 | assert.ok(RegExp(/No CartoCSS transform path/).exec(e),
271 | 'Unexpected exception message ' + e);
272 | });
273 |
274 | // -----------------------------------------------------------------
275 | // setLayerName
276 | // -----------------------------------------------------------------
277 |
278 | // See https://github.com/Vizzuality/grainstore/issues/54
279 | it('layername replacement', function () {
280 | var s = t.setLayerName("#t{ [l='1']{ marker-line-color: #FFF;} [l='2'] { marker-line-color: #FF1;} }", 'layer0');
281 | var e = "#layer0 { [l='1']{ marker-line-color: #FFF;} [l='2'] { marker-line-color: #FF1;} }";
282 | assert.equal(s, e);
283 | });
284 |
285 | // See https://github.com/Vizzuality/grainstore/issues/57
286 | it('layername replacement with labels', function () {
287 | var s = t.setLayerName('#t { marker-color: #FFF; } #t::l1 { text-name: [name]; }', 'layer0');
288 | var e = '#layer0 { marker-color: #FFF; } #layer0 ::l1 { text-name: [name]; }';
289 | assert.equal(s, e);
290 | });
291 |
292 | it('setLayerName retains quotes in CartoCSS', function () {
293 | var s = t.setLayerName("#t [t=\"ja'ja\\\"ja\"] { }", 's');
294 | var e = "#s [t=\"ja'ja\\\"ja\"] { }";
295 | assert.equal(s, e);
296 |
297 | s = t.setLayerName("#t [t='ja\\'ja\"ja'] { }", 's');
298 | e = "#s [t='ja\\'ja\"ja'] { }";
299 | assert.equal(s, e);
300 | });
301 |
302 | // after(function() { });
303 | });
304 |
--------------------------------------------------------------------------------
/NEWS.md:
--------------------------------------------------------------------------------
1 | Version 3.0.0
2 | 2020-05-13
3 |
4 | - Updates millstone to version v0.6.17-carto.4
5 | - Support Node.js 12
6 | - Drop support for Node.js < 12
7 | - Upgrade `mocha` to version `7.1.2`
8 | - Uses the default BDD test style
9 | - Simplify CI configuration
10 |
11 | Version 2.0.1
12 | 2019-07-15
13 |
14 | - Fix `TypeError: err.includes is not a function` #154
15 | - Update millstone to v0.6.17-carto.3
16 |
17 | Version 2.0.0
18 | 2019-03-29
19 |
20 | - Drop support for Node.js 6 and 8
21 | - Handle uncaught exception when an error happens while creating child mml-builder process
22 | - Upgrade `libxmljs` to version 0.19.5
23 |
24 | Version 1.11.0
25 | 2019-03-13
26 | - Do not hang when child process is not able to generate a Mapnik XML
27 | - Set a default timeout of 5s, configurable through `worker_timeout` param. You can disable it by setting `worker_timeout: 0`
28 | - Handle ENOMEM error as special error (emits a custom event to the process)
29 |
30 | Version 1.10.0
31 | 2018-11-20
32 | - Support Node.js 8 and 10.
33 | - Use a forked version of millstone that is compatible with Node.js 10 LTS
34 | - Add package-lock.json
35 |
36 | Version 1.9.2
37 | 2018-10-25
38 | - Make all modules to use strict mode semantics.
39 |
40 | Version 1.9.1
41 | 2018-10-23
42 | - Prevent from uncaught exception: do not disconnect child process if it's already disconnected.
43 |
44 | Version 1.9.0
45 | 2018-04-05
46 | - Add support for `markers_symbolizer_caches` parameter to allow disabling them. See https://github.com/CartoDB/mapnik/pull/43
47 |
48 | Version 1.8.2
49 | 2018-01-30
50 | - Add support for `minimum-scale-denominator`, `maximum-scale-denominator`, `queryable`, `clear-label-cache`, `cache-features`, `group-by`, `buffer-size` and `maximum-extent` options in layers. The options can be set for all layers (passing a single object as the parameter) or per layer (passing an array with as many objects as layers).
51 |
52 |
53 | Version 1.8.1
54 | 2018-01-11
55 | - Fix: do not attempt to transform styles between mapnik v3.0.12 and v3.0.15
56 |
57 |
58 | Version 1.8.0
59 | 2018-01-02
60 | - Introduces support for minzoom and maxzoom in layers.
61 |
62 |
63 | Version 1.7.0
64 | 2017-12-01
65 | - Allow to generate a valid XML without styles (no cartocss) #139.
66 |
67 |
68 | Version 1.6.4
69 | 2017-10-04
70 | - Upgrade debug to 3.1.0.
71 |
72 |
73 | Version 1.6.3
74 | 2017-04-24
75 | - While transforming styles from 2.3.x to 3.x version do not set property to default value if parent selector has the same property already defined #137.
76 |
77 |
78 | Version 1.6.2
79 | 2017-03-29
80 | - Fix issue when creating unused workers at startup time #134.
81 |
82 |
83 | Version 1.6.1
84 | 2017-03-17
85 | - Define a default map maximum-extent property.
86 |
87 |
88 | Version 1.6.0
89 | 2017-03-02
90 | - Now MMLBuilder renders XMLs outside of the main event loop setting `use_workers` config param to `true` #132
91 | - Allow overriding layer's SRID #126
92 |
93 |
94 | Version 1.5.1
95 | 2017-02-06
96 |
97 | - Style translator, `building-fill` default color back to gray #122.
98 | - Style translator, allow to use url without quotes #121.
99 | - Style translator, fix typo #120.
100 |
101 |
102 | Version 1.5.0
103 | 2017-01-27
104 |
105 | - Support Mapnik Reference 3.0.x
106 |
107 |
108 | Version 1.4.0
109 | 2016-12-13
110 |
111 | - Improve debugging: DEBUG=grainstore:renderer:xml will output the final generated XML.
112 |
113 |
114 | Version 1.3.0
115 | 2016-12-01
116 |
117 | - Add option to render XML with a child process, not blocking the main thread.
118 |
119 |
120 | Version 1.2.0
121 | 2016-06-07
122 |
123 | - Allow to provide an array of ids to override layer ids/names #111
124 | - Upgrade libxmljs development dependency to 0.18.0
125 | - Testing/working with Node.js 0.10.x and Node.js 0.12.x
126 |
127 |
128 | Version 1.1.1
129 | 2016-02-04
130 |
131 | - Upgrades millstone version to 0.6.16
132 |
133 |
134 | Version 1.1.0
135 | 2015-10-21
136 |
137 | - Add options param in MMLStore to create MMLBuilder with extra options
138 |
139 | Version 1.0.0
140 | 2015-07-05
141 |
142 | - **IMPORTANT**: backwards incompatible release: to keep using previous API/behaviour stick to 0.x series
143 | - Simpler API with just allow to get XML from params
144 | - No more style storing in Redis
145 |
146 |
147 | Version 0.23.0
148 | 2015-02-10
149 |
150 | - Allow specifying/modifying datasource per layer
151 |
152 | Version 0.22.1
153 | 2014-12-02
154 |
155 | - Freezes millstone version to 0.6.14 to support node.js v0.8.x
156 |
157 | Version 0.22.0
158 | 2014-09-23
159 |
160 | - Adds transformation path to from 2.2.0 to 2.3.0
161 |
162 | Version 0.21.0
163 | 2014-09-16
164 |
165 | - Removes unused base64
166 | - Upgrades dependencies
167 | - semver
168 | - millstone
169 | - Removes mapnik-reference dependency
170 | - Allow specifying a extra datasource options for each layer
171 | - Allow specifying a column type for each layer (#93)
172 |
173 | Version 0.20.0
174 | 2014-08-13
175 |
176 | - Upgrades dependencies:
177 | - underscore
178 | - redis-mpool
179 | - Specifies name in the redis pool
180 |
181 | Version 0.19.0
182 | 2014-08-07
183 |
184 | - Allow specifying a column name for each layer (#92)
185 | - Allow specifying different formats for tile via optional args
186 |
187 | Version 0.18.2
188 | 2014-07-02
189 |
190 | - Allows specifiying search_path when building the MML
191 | - How to release documentation
192 | - License update
193 |
194 | Version 0.18.1
195 | 2014-03-04
196 |
197 | - Stop using WATCH (#89)
198 | - Require redis-mpool ~0.0.4
199 |
200 | Version 0.18.0
201 | 2014-02-13
202 |
203 | - Augment verbosity of localized resource purges
204 | - Drop support for configuration options "gc_prob" and "ttl"
205 | - Stop storing table-less configs in redis (#86)
206 | - Drop support for constructing MMLBuilder by token
207 | - Drop MMLStore.gc() method
208 | - Drop MMLBuilder.touch() method
209 | - Tested to work with node-0.10.26 (#87)
210 |
211 | Version 0.17.1
212 | 2014-02-11
213 |
214 | - Stop scanning "related keys" on setStyle and delStyle (#84)
215 |
216 | Version 0.17.0
217 | 2014-01-24
218 |
219 | - Use a single cache of localized resources, purge after
220 | 1 day of no access (#82)
221 | - Allow setting TTL=0 to skip storage of table-less tokens (#81)
222 | - Allow passing own RedisPooler to MMLStore ctor (#76)
223 |
224 | Version 0.16.0
225 | 2014-01-14
226 |
227 | - Stop caching XML in redis (#79, #80)
228 | - Do not include XML in MMLBuilder.getStyle return (#25)
229 | - Use external module for redis pooling (#75)
230 | - Install carto dependency via http (#77)
231 |
232 | Version 0.15.2
233 | 2013-12-05
234 |
235 | - Allow constructing MMLBuilder against bogus redis layergroup (#73)
236 | - Fix MMLBuilder construction by token after mapnik version change (#73)
237 |
238 | Version 0.15.1
239 | 2013-11-28
240 |
241 | - Fix carto renderer configuration pollution between calls
242 |
243 | Version 0.15.0
244 | 2013-11-28
245 |
246 | - Accept carto renderer configuration, allowing (among other things)
247 | providing a list of font names for validation (#72)
248 | - Allow overriding db host and port in MMLBuilder constructor (#70)
249 |
250 | Version 0.14.3
251 | 2013-11-26
252 |
253 | - Allow constructing MMLBuilder against bogus redis css (#71)
254 |
255 | Version 0.14.2
256 | 2013-11-07
257 |
258 | - Fix support for exponential notation in filter values (#69)
259 | - Fix expiration time logging unit (#68)
260 |
261 | Version 0.14.1
262 | 2013-10-31
263 |
264 | - Inherit default PostgreSQL connection parameters from libpq
265 |
266 | Version 0.14.0
267 | 2013-10-29
268 |
269 | - Add support for Mapnik-2.2.0 (#67)
270 |
271 | Version 0.13.12
272 | 2013-10-10
273 |
274 | - Restore support for node-0.8.9, relaxing millstone dep (#64)
275 | (requires deployers to use srs-0.3.2)
276 |
277 | Version 0.13.11
278 | 2013-10-03
279 |
280 | - Fix support for apostrophes in CartoCSS filters (#63)
281 | - Back to tracking mainstream millstone (~0.6.5)
282 |
283 | Version 0.13.10
284 | 2013-09-12
285 |
286 | - Fix error message for invalid content in non-selfclosing tags (#62)
287 |
288 | Version 0.13.9
289 | 2013-09-09
290 |
291 | - Fix support for use of space-prefixed "zoom" in CartoCSS (#61)
292 |
293 | Version 0.13.8
294 | 2013-09-04
295 |
296 | - Fix race condition in external resource localization (#60)
297 |
298 | Version 0.13.7
299 | 2013-08-13
300 |
301 | - Change 'layer#' to 'style#' in blank CartoCSS error (#59)
302 |
303 | Version 0.13.6
304 | 2013-07-16
305 |
306 | - Error out on blank CartoCSS array element (#59)
307 |
308 | Version 0.13.5
309 | 2013-06-28
310 |
311 | - Fix support for CartoCSS attachments (#57)
312 | - Expose a knownByRedis property to MMLBuilder instances
313 |
314 | Version 0.13.4
315 | 2013-06-12
316 |
317 | - Upgrade redis to 0.8.3
318 | - Fix deadlock during GC (#55)
319 |
320 | Version 0.13.3
321 | 2013-06-06
322 |
323 | - More fixes to code replacing layer name (#54)
324 |
325 | Version 0.13.2
326 | 2013-05-29
327 |
328 | - Do not confuse colors with layer names (#53)
329 |
330 | Version 0.13.1
331 | 2013-05-29
332 |
333 | - Fix handling of multiple style sections (#53)
334 |
335 | Version 0.13.0
336 | 2013-04-04
337 |
338 | WARNING: this version changes expected format for "interactivity"
339 | option introduced in 0.12.0
340 |
341 | - Upgrade generic-pool requirement to ~2.0.3
342 | - Add support for per-layer "interactivity" option (array)
343 |
344 | Version 0.12.0
345 | 2013-03-29
346 |
347 | - Add support for "interactivity" option (#51)
348 |
349 | Version 0.11.2
350 | 2013-03-22
351 |
352 | - Fix use of ampersend characters in CartoCSS (#50)
353 | - Fix async error on invalid .touch() call
354 |
355 | Version 0.11.1
356 | 2013-02-25
357 |
358 | - Cleanly handle redis key disappearance on MMLBuilder.getStyle
359 | - Fix MMLBuilder construction error handling in GC
360 |
361 | Version 0.11.0 - Multilayer
362 | 2013-02-14
363 |
364 | - Tracking upstream mapnik-reference ~5.0.4
365 | - MMLBuilder constructor callback is now mandatory
366 | - Redis keys for extended styles use md5 hash now
367 | - Check redis connection at pool creation time
368 | - Multilayer support:
369 | - Support arrays for "sql", "style" and "style_version" parameters (#48)
370 | - New getToken() API method
371 | - New touch() API method
372 | - Probabilistic based garbage collection for table-less styles
373 |
374 | Version 0.10.9 - "EOW"
375 | 2012-12-21
376 |
377 | - Reduce default extent to allow for consistent proj4
378 | round-tripping (#42)
379 | - Revert marker-multi-policy to 'whole' now that
380 | centroid computation for multi was fixed in mapnik-2.1.x
381 | - Do not force line-clip and polygon-clip to false (#46)
382 |
383 | Version 0.10.8
384 | 2012-11-28
385 |
386 | - Use marker-multi-policy:largest as a workaround for
387 | mapnik bug in centroid computation (#44)
388 |
389 | Version 0.10.7
390 | 2012-11-28
391 |
392 | - Fix comments stripping during CartoCSS transform (#41)
393 | - Fix default extent to be full webmercator extent (#42)
394 | - Enhance 2.0 -> 2.1 style transform
395 | - Convert marker-opacity to marker-fill-opacity (#40)
396 |
397 | Version 0.10.6
398 | 2012-11-28
399 |
400 | - Enhance 2.0 -> 2.1 style transform
401 | - Consider directives in blocks with no conditions as applying
402 | to all blocks. Fixes issue #39
403 |
404 | Version 0.10.5
405 | 2012-11-27
406 |
407 | - For mapnik-2.1.0+, set default style version to target version
408 | - Scale arrow markers by 50% in 2.0.x -> 2.1.0 transform
409 |
410 | Version 0.10.4
411 | 2012-11-23
412 |
413 | - Fix enforcement of line symbolizer with marker-line-* (#37)
414 | - Add support CartoCSS transform from mapnik 2.0.x to 2.1.1:
415 | - marker-multi-policy:whole forced (#36)
416 |
417 | Version 0.10.3
418 | 2012-11-20
419 |
420 | - Fix eating of lines after one-line comments (#35)
421 | - Strip comments from styles before performing transformations
422 |
423 | Version 0.10.2
424 | 2012-11-20
425 |
426 | - Set line-clip:false and polygon-clip:false when transforming styles
427 | from 2.0 to 2.1 (#34)
428 |
429 | Version 0.10.1
430 | 2012-11-15
431 |
432 | - Fix transform of styles using markers conditionally (#33)
433 |
434 | Version 0.10.0
435 | 2012-11-14
436 |
437 | - Add optional "convert" parameter to MMLBuilder.getStyle() method (#32)
438 |
439 | Version 0.9.7
440 | 2012-11-13
441 |
442 | - Gracefully handle unsupported geometry types
443 | - Make default style good for any geometry type (#22)
444 |
445 | Version 0.9.6
446 | 2012-11-09
447 |
448 | - Set marker-clip:false when transforming styles from 2.0 to 2.1
449 |
450 | Version 0.9.5
451 | 2012-11-06
452 |
453 | - Fix transformation of styles with missing semicolon (#30)
454 |
455 | Version 0.9.4
456 | 2012-11-02
457 |
458 | - Add support for using "mapnik-geometry-type" in filters
459 | - Enhanced 2.0 to 2.1 style transform:
460 | - Set default marker-placement to line for lines and polygons
461 | - Set marker-type to 'ellipse' when marker-pacement is 'point'
462 |
463 | Version 0.9.3
464 | 2012-10-30
465 |
466 | - Fix race condition resulting in effects of setStyle occasionally
467 | overridden by existing styl repairing code at read time (#27)
468 | - Fix "make check" return status
469 |
470 | Version 0.9.2
471 | 2012-10-19
472 |
473 | - Fix mml_builder constructor to accept a style_version parameter
474 |
475 | Version 0.9.1
476 | 2012-10-09
477 |
478 | - Add xml_version to rendered style caches
479 | - Regenerate XML cache on xml_version missing or mismatch
480 |
481 | Version 0.9.0
482 | 2012-10-09
483 |
484 | - Add grainstore.version() API method
485 |
486 | Version 0.8.1
487 | 2012-10-08
488 |
489 | - Add support for 2.0.3 in style transformer
490 |
491 | Version 0.8.0
492 | 2012-10-08
493 |
494 | - Support requesting conversion with .setStyle and .resetStyle
495 | - Add --convert switch to tools/reset_styles for batch-conversion
496 |
497 | Version 0.7.0
498 | 2012-09-28
499 |
500 | - Style versioning
501 | - Allow specifying style version in .setStyle
502 | - Return style version from .getStyle
503 | - Include style version in store
504 | - Allow specifying target mapnik version
505 | - Transform CartoCSS to target mapnik version on rendering
506 | - Only store CartoCSS in the base redis key (#23)
507 | - Back to tracking mainstream millstone (~0.5.9)
508 | - Back to tracking mainstream carto (~0.9.2)
509 | - Many more tests for custom style storage in redis
510 | - Add tools/reset_styles script to reset all styles
511 |
512 | Version 0.6.4
513 | 2012-09-20
514 |
515 | - Target a branch of millstone that fixes support for urls
516 | containing regexp control chars in them (most notably '+')
517 |
518 | Version 0.6.3
519 | 2012-09-20
520 |
521 | - Fix MMBuilder.resetStyle (and add proper testcase)
522 |
523 | Version 0.6.2
524 | 2012-09-19
525 |
526 | NOTE: when upgrading from 0.6.0 you will need to rename any existing
527 | localized resources directories or force re-rendering of the
528 | XML styles.
529 |
530 | - Add MMBuilder.resetStyle method to re-render an XML style
531 | - Reconstruct the XML when lost in redis
532 | - Change localized resources path from -
533 | to /.
534 |
535 | Version 0.6.1
536 | 2012-09-18
537 |
538 | - Fix support for node-0.4
539 |
540 | Version 0.6.0
541 | 2012-09-14
542 |
543 | API changes (backward compatible):
544 | - Automatically localise external resources in CartoCSS (#4)
545 |
546 | Version 0.5.0
547 | 2012-09-03
548 |
549 | API changes:
550 | - Exclude database username in redis style cache key (introduced in 0.3)
551 |
552 | Version 0.4.0
553 | 2012-08-13
554 |
555 | API changes (backward compatible):
556 | - Add a mapnik_version option in MMLBuilder constructor
557 | Other changes:
558 | - Add more CartoCSS parsing and conversion tests
559 | Bug fixes:
560 | - Accept 'point-transform' without 'point-file' (#20)
561 |
562 | Version 0.3.1
563 | 2012-07-25
564 |
565 | - Loosen carto dependency to include the 0.8 series
566 | - Drop 'srs' dependency, use "+init=epsg:xxx" in map XML to
567 | allow mapnik do special handling of wgs84->webmercator reprojection
568 |
569 | Version 0.3.0
570 | 2012-07-12
571 |
572 | API changes:
573 | - Add optional callback parameter to MMLBuilder constructors
574 | - Allow overriding db authentication with mml_builder options
575 | - Include database username in redis style cache key
576 |
577 | Version 0.2.4
578 | 2012-07-11
579 |
580 | - Improve testsuite to automatically start redis (#14)
581 | - Clarify licensing terms (#15)
582 | - Loosen undescrore requirements to 1.1 - 1.3
583 | - Require mocha 1.2.1 as 1.2.2 doesn't work with node-0.4
584 | ( https://github.com/visionmedia/mocha/issues/489 )
585 | - Require hiredis 0.1.14 for OSX 10.7 support
586 | ( https://github.com/Vizzuality/Windshaft-cartodb/issues/14 )
587 |
588 | Version 0.2.3
589 | 2012-07-03
590 |
591 | - Tests ported to mocha (#11)
592 | - Require libxmljs-0.5.x and redis-0.7.2 (for node-0.8.x compatibility)
593 |
594 | Version 0.2.2
595 | 2012-06-26
596 |
597 | - Require leaks free 'carto' 0.7.0 and 'srs' 0.2.14 (#12)
598 | - Testsuite improvements:
599 | - Add support for make check
600 | - Fix invalid syntax used in tests for mml_builder (#13)
601 | - Print unexpected error message in mml_buider test
602 |
603 |
604 | Version 0.2.1
605 | 2012-06-06
606 |
607 | Version 0.2.0
608 | 2011-12-08
609 |
610 | Version 0.0.12
611 | 2011-11-30
612 |
613 | Version 0.0.11
614 | 2011-11-25
615 |
616 | Version 0.0.10
617 | 2011-10-07
618 |
619 | Version 0.0.9
620 | 2011-09-20
621 |
622 | Version 0.0.8
623 | 2011-09-20
624 |
625 | Version 0.0.7
626 | 2011-09-14
627 |
628 | Version 0.0.6
629 | 2011-09-06
630 |
631 | Version 0.0.5
632 | 2011-09-04
633 |
634 | Version 0.0.4
635 | 2011-08-15
636 |
637 | Version 0.0.3
638 | 2011-08-15
639 |
640 | Version 0.0.2
641 | 2011-08-11
642 |
643 | Version 0.0.1
644 | 2011-08-11
645 |
--------------------------------------------------------------------------------
/test/mml_builder_multilayer.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var assert = require('assert');
4 | var grainstore = require('../lib/grainstore');
5 | var step = require('step');
6 | var http = require('http');
7 | var fs = require('fs');
8 |
9 | const xml2js = require('xml2js');
10 | const xpath = require('xml2js-xpath');
11 |
12 | var server;
13 |
14 | var serverPort = 8033;
15 |
16 | var DEFAULT_POINT_STYLE = [
17 | '#layer {',
18 | ' marker-fill: #FF6600;',
19 | ' marker-opacity: 1;',
20 | ' marker-width: 16;',
21 | ' marker-line-color: white;',
22 | ' marker-line-width: 3;',
23 | ' marker-line-opacity: 0.9;',
24 | ' marker-placement: point;',
25 | ' marker-type: ellipse;',
26 | ' marker-allow-overlap: true;',
27 | '}'
28 | ].join('');
29 |
30 | [false, true].forEach(function (useWorkers) {
31 | describe('mml_builder multilayer use_workers=' + useWorkers, function () {
32 | var queryMakeLine = 'SELECT ST_MakeLine(ST_MakePoint(-10,-5),ST_MakePoint(10,-5))';
33 | var queryMakePoint = 'SELECT ST_MakePoint(0,0)';
34 |
35 | var styleLine = '#layer1 { line-color:red; }';
36 | var stylePoint = '#layer0 { marker-width:3; }';
37 |
38 | before(function (done) {
39 | // Start a server to test external resources
40 | server = http.createServer(function (request, response) {
41 | var filename = 'test/support/resources' + request.url;
42 | fs.readFile(filename, 'binary', function (err, file) {
43 | if (err) {
44 | response.writeHead(404, { 'Content-Type': 'text/plain' });
45 | console.log("File '" + filename + "' not found");
46 | response.write('404 Not Found\n');
47 | } else {
48 | response.writeHead(200);
49 | response.write(file, 'binary');
50 | }
51 | response.end();
52 | });
53 | });
54 | server.listen(serverPort, done);
55 | });
56 |
57 | after(function () {
58 | server.close();
59 | });
60 |
61 | it('accept sql array with style array', function (done) {
62 | var style0 = '#layer0 { marker-width:3; }';
63 | var style1 = '#layer1 { line-color:red; }';
64 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers, mapnik_version: '2.1.0' });
65 |
66 | step(
67 | function initBuilder () {
68 | mmlStore.mml_builder({
69 | dbname: 'my_database',
70 | sql: ['SELECT ST_MakePoint(0,0)', 'SELECT ST_MakeLine(ST_MakePoint(-10,-5),ST_MakePoint(10,-5))'],
71 | style: [style0, style1],
72 | style_version: '2.1.0'
73 | }).toXML(this);
74 | },
75 | function checkXML0 (err, xml) {
76 | if (err) { done(err); return; }
77 | xml2js.parseString(xml, (err, xmlDoc) => {
78 | if (err) { done(err); return; }
79 |
80 | const layer0 = xpath.find(xmlDoc, '//Layer').find(l => l.$.name === 'layer0');
81 | assert.ok(layer0);
82 |
83 | let geomField = layer0.Datasource[0].Parameter.find(param => param.$.name === 'geometry_field');
84 | assert.equal(geomField._, 'the_geom_webmercator');
85 |
86 | const layer1 = xpath.find(xmlDoc, '//Layer').find(l => l.$.name === 'layer1');
87 | assert.ok(layer1);
88 |
89 | geomField = layer1.Datasource[0].Parameter.find(param => param.$.name === 'geometry_field');
90 | assert.equal(geomField._, 'the_geom_webmercator');
91 |
92 | const style0 = xpath.find(xmlDoc, '//Style').find(s => s.$.name === 'layer0');
93 | assert.ok(style0);
94 |
95 | const style1 = xpath.find(xmlDoc, '//Style').find(s => s.$.name === 'layer1');
96 | assert.ok(style1);
97 |
98 | done();
99 | });
100 | }
101 | );
102 | });
103 |
104 | // See http://github.com/CartoDB/grainstore/issues/92
105 | it('accept sql array with style array and gcols array', function (done) {
106 | var style0 = '#layer0 { marker-width:3; }';
107 | var style1 = '#layer1 { line-color:red; }';
108 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers, mapnik_version: '2.1.0' });
109 |
110 | step(
111 | function initBuilder () {
112 | mmlStore.mml_builder({
113 | dbname: 'my_database',
114 | sql: ['SELECT ST_MakePoint(0,0) g', 'SELECT ST_MakeLine(ST_MakePoint(-10,-5),ST_MakePoint(10,-5)) g2'],
115 | style: [style0, style1],
116 | gcols: [null, 'g2'], // first intentionally blank
117 | style_version: '2.1.0'
118 | }).toXML(this);
119 | },
120 | function checkXML0 (err, xml) {
121 | if (err) { done(err); return; }
122 |
123 | xml2js.parseString(xml, (err, xmlDoc) => {
124 | if (err) { done(err); return; }
125 |
126 | const layer0 = xpath.find(xmlDoc, '//Layer').find(l => l.$.name === 'layer0');
127 | assert.ok(layer0);
128 | assert.equal(layer0.Datasource.length, 1);
129 | let geomField = layer0.Datasource[0].Parameter.find(param => param.$.name === 'geometry_field');
130 | assert.equal(geomField._, 'the_geom_webmercator');
131 |
132 | const layer1 = xpath.find(xmlDoc, '//Layer').find(l => l.$.name === 'layer1');
133 | assert.ok(layer1);
134 | assert.equal(layer1.$.name, 'layer1');
135 |
136 | geomField = layer1.Datasource[0].Parameter.find(param => param.$.name === 'geometry_field');
137 | assert.equal(geomField._, 'g2');
138 |
139 | done();
140 | });
141 | }
142 | );
143 | });
144 |
145 | [
146 | [
147 | { type: 'geometry', name: 'g' },
148 | { type: 'raster', name: 'r' }
149 | ],
150 | [
151 | 'g',
152 | { type: 'raster', name: 'r' }
153 | ],
154 | [
155 | { name: 'g' },
156 | { type: 'raster', name: 'r' }
157 | ]
158 | ].forEach(function (gcols) {
159 | // See http://github.com/CartoDB/grainstore/issues/93
160 | it('accept types in gcols', function (done) {
161 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers, mapnik_version: '2.1.0' });
162 |
163 | step(
164 | function initBuilder () {
165 | mmlStore.mml_builder({
166 | dbname: 'my_database',
167 | sql: ['SELECT ST_MakePoint(0,0) g',
168 | 'SELECT ST_AsRaster(ST_MakePoint(0,0),1.0,1.0) r'],
169 | style: [DEFAULT_POINT_STYLE, DEFAULT_POINT_STYLE],
170 | gcols: gcols,
171 | style_version: '2.1.0'
172 | }).toXML(this);
173 | },
174 | function checkXML0 (err, xml) {
175 | if (err) { done(err); return; }
176 | xml2js.parseString(xml, (err, xmlDoc) => {
177 | if (err) { done(err); return; }
178 |
179 | const layer0 = xpath.find(xmlDoc, '//Layer').find(l => l.$.name === 'layer0');
180 | assert.ok(layer0);
181 | assert.equal(layer0.Datasource.length, 1);
182 | const geomField = layer0.Datasource[0].Parameter.find(param => param.$.name === 'geometry_field');
183 | assert.equal(geomField._, 'g');
184 |
185 | const layer1 = xpath.find(xmlDoc, '//Layer').find(l => l.$.name === 'layer1');
186 | assert.ok(layer1);
187 | assert.equal(layer1.Datasource.length, 1);
188 |
189 | const rasterField = layer1.Datasource[0].Parameter.find(param => param.$.name === 'raster_field');
190 | const type = layer1.Datasource[0].Parameter.find(param => param.$.name === 'type');
191 | assert.equal(rasterField._, 'r');
192 | assert.equal(type._, 'pgraster');
193 |
194 | done();
195 | });
196 | }
197 | );
198 | });
199 | });
200 |
201 | // See http://github.com/CartoDB/grainstore/issues/93
202 | it('accept rcolbands and extra_ds_opts arrays', function (done) {
203 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers, mapnik_version: '2.1.0' });
204 |
205 | step(
206 | function initBuilder () {
207 | mmlStore.mml_builder({
208 | dbname: 'my_database',
209 | sql: ['SELECT ST_MakePoint(0,0) g',
210 | 'SELECT ST_AsRaster(ST_MakePoint(0,0),1.0,1.0) r',
211 | 'SELECT ST_AsRaster(ST_MakePoint(0,0),1.0,1.0) r2'],
212 | style: [DEFAULT_POINT_STYLE, DEFAULT_POINT_STYLE, DEFAULT_POINT_STYLE],
213 | gcols: [
214 | { type: 'geometry', name: 'g' },
215 | { type: 'raster', name: 'r' },
216 | { type: 'raster', name: 'r2' }
217 | ],
218 | extra_ds_opts: [
219 | { geometry_field: 'fake' }, // will not override
220 | { use_overviews: 1, prescale_rasters: true },
221 | { band: 1, clip_rasters: 1 }
222 | ],
223 | style_version: '2.1.0'
224 | }).toXML(this);
225 | },
226 | function checkXML0 (err, xml) {
227 | if (err) { done(err); return; }
228 | xml2js.parseString(xml, (err, xmlDoc) => {
229 | if (err) { done(err); return; }
230 |
231 | const layer0 = xpath.find(xmlDoc, '//Layer').find(l => l.$.name === 'layer0');
232 | assert.ok(layer0);
233 | assert.equal(layer0.Datasource.length, 1);
234 | const geomField = layer0.Datasource[0].Parameter.find(param => param.$.name === 'geometry_field');
235 | assert.equal(geomField._, 'g');
236 |
237 | const layer1 = xpath.find(xmlDoc, '//Layer').find(l => l.$.name === 'layer1');
238 | assert.ok(layer1);
239 | assert.equal(layer1.Datasource.length, 1);
240 | let rasterField = layer1.Datasource[0].Parameter.find(param => param.$.name === 'raster_field');
241 | let type = layer1.Datasource[0].Parameter.find(param => param.$.name === 'type');
242 | let band = layer1.Datasource[0].Parameter.find(param => param.$.name === 'band');
243 | let clipCasters = layer1.Datasource[0].Parameter.find(param => param.$.name === 'clip_rasters');
244 | let useOverviews = layer1.Datasource[0].Parameter.find(param => param.$.name === 'use_overviews');
245 | let prescaleRasters = layer1.Datasource[0].Parameter.find(param => param.$.name === 'prescale_rasters');
246 | assert.equal(rasterField._, 'r');
247 | assert.equal(type._, 'pgraster');
248 | assert.equal(band, undefined);
249 | assert.equal(clipCasters, undefined);
250 | assert.equal(useOverviews._, '1');
251 | assert.equal(prescaleRasters._, 'true');
252 |
253 | const layer2 = xpath.find(xmlDoc, '//Layer').find(l => l.$.name === 'layer2');
254 | assert.ok(layer2);
255 | assert.equal(layer2.Datasource.length, 1);
256 | rasterField = layer2.Datasource[0].Parameter.find(param => param.$.name === 'raster_field');
257 | type = layer2.Datasource[0].Parameter.find(param => param.$.name === 'type');
258 | band = layer2.Datasource[0].Parameter.find(param => param.$.name === 'band');
259 | clipCasters = layer2.Datasource[0].Parameter.find(param => param.$.name === 'clip_rasters');
260 | useOverviews = layer2.Datasource[0].Parameter.find(param => param.$.name === 'use_overviews');
261 | prescaleRasters = layer2.Datasource[0].Parameter.find(param => param.$.name === 'prescale_rasters');
262 | assert.equal(rasterField._, 'r2');
263 | assert.equal(type._, 'pgraster');
264 | assert.equal(band._, '1');
265 | assert.equal(clipCasters._, '1');
266 | assert.equal(useOverviews, undefined);
267 | assert.equal(prescaleRasters, undefined);
268 |
269 | done();
270 | });
271 | }
272 | );
273 | });
274 |
275 | it('gcol with objects fails when name is not provided', function (done) {
276 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers, mapnik_version: '2.1.0' });
277 |
278 | step(
279 | function initBuilder () {
280 | mmlStore.mml_builder({
281 | dbname: 'my_database',
282 | sql: ['SELECT ST_MakePoint(0,0) g',
283 | 'SELECT ST_AsRaster(ST_MakePoint(0,0),1.0,1.0) r'],
284 | style: [DEFAULT_POINT_STYLE, DEFAULT_POINT_STYLE],
285 | gcols: [
286 | { type: 'geometry' }
287 | ],
288 | style_version: '2.1.0'
289 | }).toXML(this);
290 | },
291 | function getXML0 (err) {
292 | assert.ok(!!err);
293 | done();
294 | }
295 | );
296 | });
297 |
298 | it('datasource_extend option allows to have different datasources per layer', function (done) {
299 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers, mapnik_version: '2.3.0' });
300 |
301 | var defaultUser = 'default_user';
302 | var defaultPass = 'default_pass';
303 | var wadusUser = 'wadus_user';
304 | var wadusPass = 'wadus_password';
305 |
306 | var datasourceExtend = {
307 | user: wadusUser,
308 | password: wadusPass
309 | };
310 |
311 | mmlStore.mml_builder({
312 | dbuser: defaultUser,
313 | dbpassword: defaultPass,
314 | dbname: 'my_database',
315 | sql: [queryMakeLine, queryMakePoint],
316 | datasource_extend: [null, datasourceExtend],
317 | style: [styleLine, stylePoint],
318 | style_version: '2.3.0'
319 | }).toXML((err, xml) => {
320 | if (err) { done(err); return; }
321 |
322 | xml2js.parseString(xml, (err, xmlDoc) => {
323 | if (err) { done(err); return; }
324 |
325 | const layer0 = xpath.find(xmlDoc, '//Layer').find(l => l.$.name === 'layer0');
326 | assert.ok(layer0);
327 | assert.equal(layer0.Datasource.length, 1);
328 |
329 | const layer1 = xpath.find(xmlDoc, '//Layer').find(l => l.$.name === 'layer1');
330 | assert.ok(layer1);
331 | assert.equal(layer1.Datasource.length, 1);
332 |
333 | let user = layer0.Datasource[0].Parameter.find(param => param.$.name === 'user');
334 | let password = layer0.Datasource[0].Parameter.find(param => param.$.name === 'password');
335 | assert.equal(user._, defaultUser);
336 | assert.equal(password._, defaultPass);
337 |
338 | user = layer1.Datasource[0].Parameter.find(param => param.$.name === 'user');
339 | password = layer1.Datasource[0].Parameter.find(param => param.$.name === 'password');
340 | assert.equal(user._, wadusUser);
341 | assert.equal(password._, wadusPass);
342 |
343 | return done();
344 | });
345 | });
346 | });
347 |
348 | it('error out on blank CartoCSS in a style array', function (done) {
349 | var style0 = '#layer0 { marker-width:3; }';
350 | var style1 = '';
351 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers, mapnik_version: '2.1.0' });
352 |
353 | step(
354 | function initBuilder () {
355 | mmlStore.mml_builder({
356 | dbname: 'my_database',
357 | sql: ['SELECT ST_MakePoint(0,0)', 'SELECT ST_MakeLine(ST_MakePoint(-10,-5),ST_MakePoint(10,-5))'],
358 | style: [style0, style1],
359 | style_version: '2.1.0'
360 | }).toXML(this);
361 | },
362 | function checkError (err) {
363 | assert(err);
364 | assert.equal(err.message, 'style1: CartoCSS is empty');
365 | return null;
366 | },
367 | function finish (err) {
368 | done(err);
369 | }
370 | );
371 | });
372 |
373 | it('accept sql with style and style_version array', function (done) {
374 | var style0 = '#layer0 { marker-width:3; }';
375 | var style1 = '#layer1 { marker-width:4; }';
376 | var sql0 = 'SELECT ST_MakePoint(0,0)';
377 | var sql1 = 'SELECT ST_MakeLine(ST_MakePoint(-10,-5),ST_MakePoint(10,-5))';
378 | var styleVersion0 = '2.0.2';
379 | var styleVersion1 = '2.1.0';
380 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers, mapnik_version: '2.1.0' });
381 |
382 | step(
383 | function initBuilder () {
384 | mmlStore.mml_builder({
385 | dbname: 'my_database',
386 | sql: [sql0, sql1],
387 | style: [style0, style1],
388 | style_version: [styleVersion0, styleVersion1]
389 | }).toXML(this);
390 | },
391 | function checkXML0 (err, xml) {
392 | if (err) {
393 | throw err;
394 | }
395 | xml2js.parseString(xml, (err, xmlDoc) => {
396 | if (err) { done(err); return; }
397 |
398 | const layer0 = xpath.find(xmlDoc, '//Layer').find(l => l.$.name === 'layer0');
399 | assert.ok(layer0);
400 | assert.equal(layer0.Datasource.length, 1);
401 | const table0 = layer0.Datasource[0].Parameter.find(param => param.$.name === 'table');
402 | assert.ok(
403 | table0._.indexOf(sql0) !== -1,
404 | 'Cannot find sql [' + sql0 + '] in table datasource, got ' + table0._
405 | );
406 |
407 | const layer1 = xpath.find(xmlDoc, '//Layer').find(l => l.$.name === 'layer1');
408 | assert.ok(layer1);
409 | assert.equal(layer1.Datasource.length, 1);
410 | const table1 = layer1.Datasource[0].Parameter.find(param => param.$.name === 'table');
411 | assert.ok(
412 | table1._.indexOf(sql1) !== -1,
413 | 'Cannot find sql [' + sql1 + '] in table datasource, got ' + table1._
414 | );
415 |
416 | const style0 = xpath.find(xmlDoc, '//Style').find(s => s.$.name === 'layer0');
417 | const symb0 = style0.Rule.find(r => Object.prototype.hasOwnProperty.call(r, 'MarkersSymbolizer')).MarkersSymbolizer[0];
418 | assert.equal(symb0.$.width, 6);
419 |
420 | const style1 = xpath.find(xmlDoc, '//Style').find(s => s.$.name === 'layer1');
421 | const symb1 = style1.Rule.find(r => Object.prototype.hasOwnProperty.call(r, 'MarkersSymbolizer')).MarkersSymbolizer[0];
422 | assert.equal(symb1.$.width, 4);
423 |
424 | return done();
425 | });
426 | }
427 | );
428 | });
429 |
430 | it('layer name in style array is only a placeholder', function (done) {
431 | var style0 = '#layer { marker-width:3; }';
432 | var style1 = '#style { line-color:red; }';
433 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers, mapnik_version: '2.1.0' });
434 |
435 | step(
436 | function initBuilder () {
437 | mmlStore.mml_builder({
438 | dbname: 'my_database',
439 | sql: ['SELECT ST_MakePoint(0,0)', 'SELECT ST_MakeLine(ST_MakePoint(-10,-5),ST_MakePoint(10,-5))'],
440 | style: [style0, style1],
441 | style_version: '2.1.0'
442 | }).toXML(this);
443 | },
444 | function checkXML0 (err, xml) {
445 | if (err) { done(err); return; }
446 | xml2js.parseString(xml, (err, xmlDoc) => {
447 | if (err) { done(err); return; }
448 |
449 | const layer0 = xpath.find(xmlDoc, '//Layer').find(l => l.$.name === 'layer0');
450 | assert.ok(layer0);
451 |
452 | const layer1 = xpath.find(xmlDoc, '//Layer').find(l => l.$.name === 'layer1');
453 | assert.ok(layer1);
454 |
455 | const style0 = xpath.find(xmlDoc, '//Style').find(s => s.$.name === 'layer0');
456 | assert.ok(style0);
457 |
458 | const style1 = xpath.find(xmlDoc, '//Style').find(s => s.$.name === 'layer1');
459 | assert.ok(style1);
460 |
461 | done();
462 | });
463 | }
464 | );
465 | });
466 |
467 | it('layer name in single style is only a placeholder', function (done) {
468 | var style0 = '#layer { marker-width:3; } #layer[a=1] { marker-fill:#ff0000 }';
469 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers, mapnik_version: '2.1.0' });
470 |
471 | step(
472 | function initBuilder () {
473 | mmlStore.mml_builder({
474 | dbname: 'my_database',
475 | sql: ['SELECT ST_MakePoint(0,0)'],
476 | style: [style0],
477 | style_version: '2.1.0'
478 | }).toXML(this);
479 | },
480 | function checkXML0 (err, xml) {
481 | if (err) { done(err); return; }
482 | xml2js.parseString(xml, (err, xmlDoc) => {
483 | if (err) { done(err); return; }
484 |
485 | const layer0 = xpath.find(xmlDoc, '//Layer').find(l => l.$.name === 'layer0');
486 | assert.ok(layer0);
487 |
488 | const style0 = xpath.find(xmlDoc, '//Style').find(s => s.$.name === 'layer0');
489 | assert.ok(style0);
490 |
491 | const symb0 = style0.Rule.find(r => Object.prototype.hasOwnProperty.call(r, 'MarkersSymbolizer')).MarkersSymbolizer[0];
492 | assert.equal(symb0.$.fill, '#ff0000');
493 | assert.equal(symb0.$.width, 3);
494 |
495 | done();
496 | });
497 | }
498 | );
499 | });
500 |
501 | it('accept sql array with single style string', function (done) {
502 | var style0 = '#layer0 { marker-width:3; }';
503 | var style1 = '#layer1 { line-color:red; }';
504 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers, mapnik_version: '2.1.0' });
505 |
506 | step(
507 | function initBuilder () {
508 | mmlStore.mml_builder({
509 | dbname: 'my_database',
510 | sql: ['SELECT ST_MakePoint(0,0)', 'SELECT ST_MakeLine(ST_MakePoint(-10,-5),ST_MakePoint(10,-5))'],
511 | style: [style0, style1],
512 | style_version: '2.1.0'
513 | }).toXML(this);
514 | },
515 | function checkXML0 (err, xml) {
516 | if (err) { done(err); return; }
517 | xml2js.parseString(xml, (err, xmlDoc) => {
518 | if (err) { done(err); return; }
519 |
520 | const layer0 = xpath.find(xmlDoc, '//Layer').find(l => l.$.name === 'layer0');
521 | assert.ok(layer0);
522 |
523 | const layer1 = xpath.find(xmlDoc, '//Layer').find(l => l.$.name === 'layer1');
524 | assert.ok(layer1);
525 |
526 | const style0 = xpath.find(xmlDoc, '//Style').find(s => s.$.name === 'layer0');
527 | assert.ok(style0);
528 |
529 | const style1 = xpath.find(xmlDoc, '//Style').find(s => s.$.name === 'layer1');
530 | assert.ok(style1);
531 |
532 | done();
533 | });
534 | }
535 | );
536 | });
537 |
538 | it('Error out on malformed interactivity', function (done) {
539 | var sql0 = 'SELECT 1 as a, 2 as b, ST_MakePoint(0,0)';
540 | var sql1 = 'SELECT 3 as a, 4 as b, ST_MakeLine(ST_MakePoint(-10,-5),ST_MakePoint(10,-5))';
541 | var style0 = '#layer0 { marker-width:3; }';
542 | var style1 = '#layer1 { line-color:red; }';
543 | var fullstyle = style0 + style1;
544 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers, mapnik_version: '2.1.0' });
545 | var iact0;
546 | var iact1 = ['a', 'b'];
547 |
548 | step(
549 | function initBuilder () {
550 | mmlStore.mml_builder({
551 | dbname: 'my_database',
552 | sql: [sql0, sql1],
553 | interactivity: [iact0, iact1],
554 | style: fullstyle,
555 | style_version: '2.1.0'
556 | }).toXML(this);
557 | },
558 | function checkError (err) {
559 | assert.ok(err);
560 | assert.equal(err.message, 'Invalid interactivity value type for layer 1: object');
561 | done();
562 | }
563 | );
564 | });
565 |
566 | it('Error out on malformed layer', function (done) {
567 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers, mapnik_version: '2.1.0' });
568 |
569 | step(
570 | function initBuilder () {
571 | mmlStore.mml_builder({
572 | dbname: 'my_database',
573 | sql: 'select 1',
574 | style: DEFAULT_POINT_STYLE,
575 | layer: 'cipz'
576 | }).toXML(this);
577 | },
578 | function checkError (err) {
579 | assert.ok(err);
580 | assert.equal(err.message, 'Invalid (non-integer) layer value type: cipz');
581 | done();
582 | }
583 | );
584 | });
585 |
586 | it('undefined layer id uses old `layer{index}` notation for layer name', function (done) {
587 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
588 | var mml = mmlStore.mml_builder({
589 | dbname: 'my_db',
590 | ids: ['layer-name-wadus', null, 'layer-name-top'],
591 | sql: ['select 1', 'select 2', 'select 3'],
592 | style: [DEFAULT_POINT_STYLE, DEFAULT_POINT_STYLE, DEFAULT_POINT_STYLE]
593 | });
594 |
595 | mml.toXML(function (err, data) {
596 | if (err) { done(err); return; }
597 | xml2js.parseString(data, (err, xmlDoc) => {
598 | if (err) { done(err); return; }
599 |
600 | const layer0 = xpath.find(xmlDoc, '//Layer').find(l => l.$.name === 'layer-name-wadus');
601 | assert.ok(layer0);
602 |
603 | const layer1 = xpath.find(xmlDoc, '//Layer').find(l => l.$.name === 'layer1');
604 | assert.ok(layer1);
605 |
606 | const layer2 = xpath.find(xmlDoc, '//Layer').find(l => l.$.name === 'layer-name-top');
607 | assert.ok(layer2);
608 |
609 | const style0 = xpath.find(xmlDoc, '//Style').find(s => s.$.name === 'layer-name-wadus');
610 | assert.ok(style0);
611 |
612 | const style1 = xpath.find(xmlDoc, '//Style').find(s => s.$.name === 'layer1');
613 | assert.ok(style1);
614 |
615 | const style2 = xpath.find(xmlDoc, '//Style').find(s => s.$.name === 'layer-name-top');
616 | assert.ok(style2);
617 |
618 | done();
619 | });
620 | });
621 | });
622 |
623 | it('Uses correct layer name for interactivity layer', function (done) {
624 | var sql0 = 'SELECT 1 as a, 2 as b, ST_MakePoint(0,0)';
625 | var sql1 = 'SELECT 3 as a, 4 as b, ST_MakeLine(ST_MakePoint(-10,-5),ST_MakePoint(10,-5))';
626 | var style0 = '#layer0 { marker-width:3; }';
627 | var style1 = '#layer1 { line-color:red; }';
628 | var fullstyle = style0 + style1;
629 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers, mapnik_version: '2.1.0' });
630 | var iact0 = 'a,b';
631 | var iact1 = 'c,d';
632 |
633 | var mml = mmlStore.mml_builder({
634 | dbname: 'my_database',
635 | ids: ['layer-wadus-0', 'layer-wadus-1'],
636 | layer: 1,
637 | sql: [sql0, sql1],
638 | interactivity: [iact0, iact1],
639 | style: fullstyle,
640 | style_version: '2.1.0'
641 | });
642 |
643 | mml.toXML(function (err, data) {
644 | if (err) { done(err); return; }
645 | xml2js.parseString(data, (err, xmlDoc) => {
646 | if (err) { done(err); return; }
647 | var x = xpath.find(xmlDoc, "//Parameter[@name='interactivity_layer']")[0];
648 | assert.ok(x);
649 | assert.equal(x._, 'layer-wadus-1');
650 | done();
651 | });
652 | });
653 | });
654 |
655 | it('Allows specifying per-layer SRID', function (done) {
656 | var style0 = '#layer0 { marker-width:3; }';
657 | var style1 = '#layer1 { marker-width:4; }';
658 | var sql0 = 'SELECT ST_MakePoint(0,0)';
659 | var sql1 = 'SELECT ST_MakePoint(1,1)';
660 | var styleVersion0 = '2.0.2';
661 | var styleVersion1 = '2.1.0';
662 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers, mapnik_version: '2.1.0' });
663 |
664 | step(
665 | function initBuilder () {
666 | mmlStore.mml_builder({
667 | dbname: 'my_database',
668 | sql: [sql0, sql1],
669 | style: [style0, style1],
670 | style_version: [styleVersion0, styleVersion1],
671 | datasource_extend: [{ srid: 1001 }, { srid: 1002 }]
672 | }).toXML(this);
673 | },
674 | function checkXML0 (err, xml) {
675 | if (err) {
676 | throw err;
677 | }
678 | xml2js.parseString(xml, (err, xmlDoc) => {
679 | if (err) { done(err); return; }
680 |
681 | const layer0 = xpath.find(xmlDoc, '//Layer').find(l => l.$.name === 'layer0');
682 | assert.ok(layer0);
683 | assert.equal(layer0.Datasource.length, 1);
684 | const srid0 = layer0.Datasource[0].Parameter.find(param => param.$.name === 'srid');
685 | assert.equal(srid0._, '1001');
686 |
687 | const layer1 = xpath.find(xmlDoc, '//Layer').find(l => l.$.name === 'layer1');
688 | assert.ok(layer1);
689 | assert.equal(layer1.Datasource.length, 1);
690 | const srid1 = layer1.Datasource[0].Parameter.find(param => param.$.name === 'srid');
691 | assert.equal(srid1._, '1002');
692 |
693 | return done();
694 | });
695 | }
696 | );
697 | });
698 | });
699 | });
700 |
--------------------------------------------------------------------------------
/test/style_trans_23_to_30.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var assert = require('assert');
4 | var StyleTrans = require('../lib/grainstore/style_trans.js');
5 |
6 | describe('cartocss transformation from 2.3.x to 3.0.x', function () {
7 | beforeEach(function () {
8 | this.styleTrans = new StyleTrans();
9 | });
10 |
11 | var polygonSuite = {
12 | symbolizer: 'polygon',
13 | testCases: [{
14 | description: 'should add defaults if polygon symbolizer is present with `polygon-fill` property',
15 | input: [
16 | '#layer {',
17 | ' polygon-fill: rgba(128,128,128,1);',
18 | '}'
19 | ].join('\n'),
20 | expected: [
21 | '#layer {',
22 | ' polygon-fill: rgba(128,128,128,1);',
23 | ' polygon-clip: true;',
24 | '}'
25 | ].join('\n')
26 | }, {
27 | description: 'should add defaults if polygon symbolizer is present with `polygon-opacity` property',
28 | input: [
29 | '#layer {',
30 | ' polygon-opacity: 0.5;',
31 | '}'
32 | ].join('\n'),
33 | expected: [
34 | '#layer {',
35 | ' polygon-opacity: 0.5;',
36 | ' polygon-clip: true;',
37 | '}'
38 | ].join('\n')
39 | }, {
40 | description: 'should not add `polygon-clip` default if polygon symbolizer is present and `polygon-clip` is already set to true',
41 | input: [
42 | '#layer {',
43 | ' polygon-opacity: 0.5;',
44 | ' polygon-clip: true;',
45 | '}'
46 | ].join('\n'),
47 | expected: [
48 | '#layer {',
49 | ' polygon-opacity: 0.5;',
50 | ' polygon-clip: true;',
51 | '}'
52 | ].join('\n')
53 | }, {
54 | description: 'should not add `polygon-clip` default if polygon symbolizer is present and `polygon-clip` is already set to false',
55 | input: [
56 | '#layer {',
57 | ' polygon-clip: false;',
58 | '}'
59 | ].join('\n'),
60 | expected: [
61 | '#layer {',
62 | ' polygon-clip: false;',
63 | '}'
64 | ].join('\n')
65 | }, {
66 | description: 'should add defaults if polygon symbolizer is present in two different rules',
67 | input: [
68 | '#layer {',
69 | ' polygon-fill: rgba(128,128,128,1);',
70 | '}',
71 | '#layer {',
72 | ' polygon-opacity: 0.5;',
73 | '}'
74 | ].join('\n'),
75 | expected: [
76 | '#layer {',
77 | ' polygon-fill: rgba(128,128,128,1);',
78 | ' polygon-clip: true;',
79 | '}',
80 | '#layer {',
81 | ' polygon-opacity: 0.5;',
82 | ' polygon-clip: true;',
83 | '}'
84 | ].join('\n')
85 | }, {
86 | description: 'should add defaults if polygon symbolizer is present with two different properties',
87 | input: [
88 | '#layer {',
89 | ' polygon-fill: rgba(128,128,128,1);',
90 | ' polygon-opacity: 0.5;',
91 | '}'
92 | ].join('\n'),
93 | expected: [
94 | '#layer {',
95 | ' polygon-fill: rgba(128,128,128,1);',
96 | ' polygon-opacity: 0.5;',
97 | ' polygon-clip: true;',
98 | '}'
99 | ].join('\n')
100 | }, {
101 | description: 'should add defaults if polygon symbolizer is present for layer with `::glow` modifier',
102 | input: [
103 | '#layer::glow {',
104 | ' polygon-simplify: 0.1;',
105 | '}'
106 | ].join('\n'),
107 | expected: [
108 | '#layer::glow {',
109 | ' polygon-simplify: 0.1;',
110 | ' polygon-clip: true;',
111 | '}'
112 | ].join('\n')
113 | }, {
114 | description: 'should not add polygon defaults if just polygon-pattern symbolizer is present',
115 | input: [
116 | '#layer::glow {',
117 | ' polygon-pattern-simplify: 0.1;',
118 | '}'
119 | ].join('\n'),
120 | expected: [
121 | '#layer::glow {',
122 | ' polygon-pattern-simplify: 0.1;',
123 | ' polygon-pattern-clip: true;',
124 | ' polygon-pattern-alignment: local;',
125 | '}'
126 | ].join('\n')
127 | }, {
128 | description: 'should add polygon and polygon-pattern defaults if both symbolizers are present for layer with `::glow` modifier',
129 | input: [
130 | '#layer::glow {',
131 | ' polygon-simplify: 0.1;',
132 | ' polygon-pattern-simplify: 0.1;',
133 | '}'
134 | ].join('\n'),
135 | expected: [
136 | '#layer::glow {',
137 | ' polygon-simplify: 0.1;',
138 | ' polygon-pattern-simplify: 0.1;',
139 | ' polygon-clip: true;',
140 | ' polygon-pattern-clip: true;',
141 | ' polygon-pattern-alignment: local;',
142 | '}'
143 | ].join('\n')
144 | }]
145 | };
146 |
147 | var polygonPatternSuite = {
148 | symbolizer: 'polygon-pattern',
149 | testCases: [{
150 | description: 'should add defaults if polygon-pattern symbolizer is present with `polygon-pattern-simplify-algorithm` property',
151 | input: [
152 | '#layer {',
153 | ' polygon-pattern-simplify-algorithm: zhao-saalfeld;',
154 | '}'
155 | ].join('\n'),
156 | expected: [
157 | '#layer {',
158 | ' polygon-pattern-simplify-algorithm: zhao-saalfeld;',
159 | ' polygon-pattern-clip: true;',
160 | ' polygon-pattern-alignment: local;',
161 | '}'
162 | ].join('\n')
163 | }, {
164 | description: 'should not add `polygon-pattern-clip` default if polygon-pattern symbolizer is present and `polygon-clip` is already set to true',
165 | input: [
166 | '#layer {',
167 | ' polygon-pattern-clip: true;',
168 | '}'
169 | ].join('\n'),
170 | expected: [
171 | '#layer {',
172 | ' polygon-pattern-clip: true;',
173 | ' polygon-pattern-alignment: local;',
174 | '}'
175 | ].join('\n')
176 | }, {
177 | description: 'should not add `polygon-pattern-clip` default if polygon symbolizer is present and `polygon-clip` is already set to false',
178 | input: [
179 | '#layer {',
180 | ' polygon-pattern-clip: false;',
181 | '}'
182 | ].join('\n'),
183 | expected: [
184 | '#layer {',
185 | ' polygon-pattern-clip: false;',
186 | ' polygon-pattern-alignment: local;',
187 | '}'
188 | ].join('\n')
189 | }, {
190 | description: 'should not add `polygon-pattern-alignment` default if polygon-pattern symbolizer is present and `polygon-pattern-alignment` is already set to global',
191 | input: [
192 | '#layer {',
193 | ' polygon-pattern-opacity: 0.5;',
194 | ' polygon-pattern-alignment: global;',
195 | '}'
196 | ].join('\n'),
197 | expected: [
198 | '#layer {',
199 | ' polygon-pattern-opacity: 0.5;',
200 | ' polygon-pattern-alignment: global;',
201 | ' polygon-pattern-clip: true;',
202 | '}'
203 | ].join('\n')
204 | }, {
205 | description: 'should not add `polygon-pattern-alignment` default if polygon-pattern symbolizer is present and `polygon-pattern-alignment` is already set to local',
206 | input: [
207 | '#layer {',
208 | ' polygon-pattern-clip: false;',
209 | ' polygon-pattern-alignment: local;',
210 | '}'
211 | ].join('\n'),
212 | expected: [
213 | '#layer {',
214 | ' polygon-pattern-clip: false;',
215 | ' polygon-pattern-alignment: local;',
216 | '}'
217 | ].join('\n')
218 | }, {
219 | description: 'should add defaults if polygon-pattern symbolizer is present in two different rules',
220 | input: [
221 | '#layer {',
222 | ' polygon-pattern-simplify: 0.1;',
223 | '}',
224 | '#layer {',
225 | ' polygon-pattern-opacity: 0.5;',
226 | '}'
227 | ].join('\n'),
228 | expected: [
229 | '#layer {',
230 | ' polygon-pattern-simplify: 0.1;',
231 | ' polygon-pattern-clip: true;',
232 | ' polygon-pattern-alignment: local;',
233 | '}',
234 | '#layer {',
235 | ' polygon-pattern-opacity: 0.5;',
236 | ' polygon-pattern-clip: true;',
237 | ' polygon-pattern-alignment: local;',
238 | '}'
239 | ].join('\n')
240 | }, {
241 | description: 'should add defaults if polygon-pattern symbolizer is present with two different properties',
242 | input: [
243 | '#layer {',
244 | ' polygon-pattern-simplify: 0.1;',
245 | ' polygon-pattern-opacity: 0.5;',
246 | '}'
247 | ].join('\n'),
248 | expected: [
249 | '#layer {',
250 | ' polygon-pattern-simplify: 0.1;',
251 | ' polygon-pattern-opacity: 0.5;',
252 | ' polygon-pattern-clip: true;',
253 | ' polygon-pattern-alignment: local;',
254 | '}'
255 | ].join('\n')
256 | }, {
257 | description: 'should add defaults if polygon-pattern symbolizer is present for layer with `::glow` modifier',
258 | input: [
259 | '#layer::glow {',
260 | ' polygon-pattern-simplify: 0.1;',
261 | '}'
262 | ].join('\n'),
263 | expected: [
264 | '#layer::glow {',
265 | ' polygon-pattern-simplify: 0.1;',
266 | ' polygon-pattern-clip: true;',
267 | ' polygon-pattern-alignment: local;',
268 | '}'
269 | ].join('\n')
270 | }, {
271 | description: 'should not add defaults if just polygon symbolizer is present',
272 | input: [
273 | '#layer::glow {',
274 | ' polygon-simplify: 0.1;',
275 | '}'
276 | ].join('\n'),
277 | expected: [
278 | '#layer::glow {',
279 | ' polygon-simplify: 0.1;',
280 | ' polygon-clip: true;',
281 | '}'
282 | ].join('\n')
283 | }, {
284 | description: 'should add polygon and polygon-pattern defaults if both symbolizers are present',
285 | input: [
286 | '#layer {',
287 | ' polygon-simplify: 0.1;',
288 | ' polygon-pattern-simplify: 0.1;',
289 | '}'
290 | ].join('\n'),
291 | expected: [
292 | '#layer {',
293 | ' polygon-simplify: 0.1;',
294 | ' polygon-pattern-simplify: 0.1;',
295 | ' polygon-clip: true;',
296 | ' polygon-pattern-clip: true;',
297 | ' polygon-pattern-alignment: local;',
298 | '}'
299 | ].join('\n')
300 | }]
301 | };
302 |
303 | var lineSuite = {
304 | symbolizer: 'line',
305 | testCases: [{
306 | description: 'should add defaults if line symbolizer is present with `line-width` property',
307 | input: [
308 | '#layer {',
309 | ' line-width: 0.5;',
310 | '}'
311 | ].join('\n'),
312 | expected: [
313 | '#layer {',
314 | ' line-width: 0.5;',
315 | ' line-clip: true;',
316 | '}'
317 | ].join('\n')
318 | }, {
319 | description: 'should add defaults if line symbolizer is present with `line-cap` property',
320 | input: [
321 | '#layer {',
322 | ' line-cap: round;',
323 | '}'
324 | ].join('\n'),
325 | expected: [
326 | '#layer {',
327 | ' line-cap: round;',
328 | ' line-clip: true;',
329 | '}'
330 | ].join('\n')
331 | }, {
332 | description: 'should not add `line-clip` default if line symbolizer is present and `line-clip` is already set to true',
333 | input: [
334 | '#layer {',
335 | ' line-cap: round;',
336 | ' line-clip: true;',
337 | '}'
338 | ].join('\n'),
339 | expected: [
340 | '#layer {',
341 | ' line-cap: round;',
342 | ' line-clip: true;',
343 | '}'
344 | ].join('\n')
345 | }, {
346 | description: 'should not add `line-clip` default if line symbolizer is present and `line-clip` is already set to false',
347 | input: [
348 | '#layer {',
349 | ' line-clip: false;',
350 | '}'
351 | ].join('\n'),
352 | expected: [
353 | '#layer {',
354 | ' line-clip: false;',
355 | '}'
356 | ].join('\n')
357 | }, {
358 | description: 'should add defaults if line symbolizer is present in two different rules',
359 | input: [
360 | '#layer {',
361 | ' line-width: 0.5;',
362 | '}',
363 | '#layer {',
364 | ' line-cap: round;',
365 | '}'
366 | ].join('\n'),
367 | expected: [
368 | '#layer {',
369 | ' line-width: 0.5;',
370 | ' line-clip: true;',
371 | '}',
372 | '#layer {',
373 | ' line-cap: round;',
374 | ' line-clip: true;',
375 | '}'
376 | ].join('\n')
377 | }, {
378 | description: 'should add defaults if line symbolizer is present with two different properties',
379 | input: [
380 | '#layer {',
381 | ' line-width: 0.5;',
382 | ' line-cap: round;',
383 | '}'
384 | ].join('\n'),
385 | expected: [
386 | '#layer {',
387 | ' line-width: 0.5;',
388 | ' line-cap: round;',
389 | ' line-clip: true;',
390 | '}'
391 | ].join('\n')
392 | }, {
393 | description: 'should add defaults if line symbolizer is present for layer with `::glow` modifier',
394 | input: [
395 | '#layer::glow {',
396 | ' line-cap: round;',
397 | '}'
398 | ].join('\n'),
399 | expected: [
400 | '#layer::glow {',
401 | ' line-cap: round;',
402 | ' line-clip: true;',
403 | '}'
404 | ].join('\n')
405 | }]
406 | };
407 |
408 | var linePatternSuite = {
409 | symbolizer: 'line-pattern',
410 | testCases: [{
411 | description: 'should add defaults if line-pattern symbolizer is present with `line-pattern-simplify-algorithm` property',
412 | input: [
413 | '#layer {',
414 | ' line-pattern-simplify-algorithm: visvalingam-whyatt;',
415 | '}'
416 | ].join('\n'),
417 | expected: [
418 | '#layer {',
419 | ' line-pattern-simplify-algorithm: visvalingam-whyatt;',
420 | ' line-pattern-clip: true;',
421 | '}'
422 | ].join('\n')
423 | }, {
424 | description: 'should not add `line-pattern-clip` default if line-pattern symbolizer is present and `line-pattern-clip` is already set to true',
425 | input: [
426 | '#layer {',
427 | ' line-pattern-clip: true;',
428 | '}'
429 | ].join('\n'),
430 | expected: [
431 | '#layer {',
432 | ' line-pattern-clip: true;',
433 | '}'
434 | ].join('\n')
435 | }, {
436 | description: 'should not add `line-pattern-clip` default if line-pattern symbolizer is present and `line-pattern-clip` is already set to false',
437 | input: [
438 | '#layer {',
439 | ' line-pattern-clip: false;',
440 | '}'
441 | ].join('\n'),
442 | expected: [
443 | '#layer {',
444 | ' line-pattern-clip: false;',
445 | '}'
446 | ].join('\n')
447 | }, {
448 | description: 'should add defaults if line-pattern symbolizer is present in two different rules',
449 | input: [
450 | '#layer {',
451 | ' line-pattern-simplify: 0.1;',
452 | '}',
453 | '#layer {',
454 | ' line-pattern-opacity: 0.5;',
455 | '}'
456 | ].join('\n'),
457 | expected: [
458 | '#layer {',
459 | ' line-pattern-simplify: 0.1;',
460 | ' line-pattern-clip: true;',
461 | '}',
462 | '#layer {',
463 | ' line-pattern-opacity: 0.5;',
464 | ' line-pattern-clip: true;',
465 | '}'
466 | ].join('\n')
467 | }, {
468 | description: 'should add defaults if line-pattern symbolizer is present with two different properties',
469 | input: [
470 | '#layer {',
471 | ' line-pattern-simplify: 0.1;',
472 | ' line-pattern-opacity: 0.5;',
473 | '}'
474 | ].join('\n'),
475 | expected: [
476 | '#layer {',
477 | ' line-pattern-simplify: 0.1;',
478 | ' line-pattern-opacity: 0.5;',
479 | ' line-pattern-clip: true;',
480 | '}'
481 | ].join('\n')
482 | }, {
483 | description: 'should add defaults if line-pattern symbolizer is present for layer with `::glow` modifier',
484 | input: [
485 | '#layer::glow {',
486 | ' line-pattern-simplify: 0.1;',
487 | '}'
488 | ].join('\n'),
489 | expected: [
490 | '#layer::glow {',
491 | ' line-pattern-simplify: 0.1;',
492 | ' line-pattern-clip: true;',
493 | '}'
494 | ].join('\n')
495 | }, {
496 | description: 'should not add defaults if just line symbolizer is present',
497 | input: [
498 | '#layer::glow {',
499 | ' line-simplify: 0.1;',
500 | '}'
501 | ].join('\n'),
502 | expected: [
503 | '#layer::glow {',
504 | ' line-simplify: 0.1;',
505 | ' line-clip: true;',
506 | '}'
507 | ].join('\n')
508 | }, {
509 | description: 'should add line and line-pattern defaults if both symbolizers are present',
510 | input: [
511 | '#layer {',
512 | ' line-simplify: 0.1;',
513 | ' line-pattern-simplify: 0.1;',
514 | '}'
515 | ].join('\n'),
516 | expected: [
517 | '#layer {',
518 | ' line-simplify: 0.1;',
519 | ' line-pattern-simplify: 0.1;',
520 | ' line-clip: true;',
521 | ' line-pattern-clip: true;',
522 | '}'
523 | ].join('\n')
524 | }]
525 | };
526 |
527 | var markerSuite = {
528 | symbolizer: 'marker',
529 | testCases: [{
530 | description: 'should add defaults if marker symbolizer is present with `marker-line-color` property',
531 | input: [
532 | '#layer {',
533 | ' marker-line-color: white;',
534 | '}'
535 | ].join('\n'),
536 | expected: [
537 | '#layer {',
538 | ' marker-line-color: white;',
539 | ' marker-clip: true;',
540 | ' marker-line-width: 1;',
541 | '}'
542 | ].join('\n')
543 | }, {
544 | description: 'should add defaults if marker symbolizer is present with `marker-placement` property',
545 | input: [
546 | '#layer {',
547 | ' marker-placement: interior;',
548 | '}'
549 | ].join('\n'),
550 | expected: [
551 | '#layer {',
552 | ' marker-placement: interior;',
553 | ' marker-clip: true;',
554 | '}'
555 | ].join('\n')
556 | }, {
557 | description: 'should not add `marker-clip` default if marker symbolizer is present and `marker-clip` is already set to true',
558 | input: [
559 | '#layer {',
560 | ' marker-placement: interior;',
561 | ' marker-clip: true;',
562 | '}'
563 | ].join('\n'),
564 | expected: [
565 | '#layer {',
566 | ' marker-placement: interior;',
567 | ' marker-clip: true;',
568 | '}'
569 | ].join('\n')
570 | }, {
571 | description: 'should not add `marker-clip` default if marker symbolizer is present and `marker-clip` is already set to false',
572 | input: [
573 | '#layer {',
574 | ' marker-clip: false;',
575 | '}'
576 | ].join('\n'),
577 | expected: [
578 | '#layer {',
579 | ' marker-clip: false;',
580 | '}'
581 | ].join('\n')
582 | }, {
583 | description: 'should add defaults if marker symbolizer is present in two different rules',
584 | input: [
585 | '#layer {',
586 | ' marker-line-color: white;',
587 | '}',
588 | '#layer {',
589 | ' marker-placement: interior;',
590 | '}'
591 | ].join('\n'),
592 | expected: [
593 | '#layer {',
594 | ' marker-line-color: white;',
595 | ' marker-clip: true;',
596 | ' marker-line-width: 1;',
597 | '}',
598 | '#layer {',
599 | ' marker-placement: interior;',
600 | ' marker-clip: true;',
601 | '}'
602 | ].join('\n')
603 | }, {
604 | description: 'should add defaults if marker symbolizer is present with two different properties',
605 | input: [
606 | '#layer {',
607 | ' marker-line-color: white;',
608 | ' marker-placement: interior;',
609 | '}'
610 | ].join('\n'),
611 | expected: [
612 | '#layer {',
613 | ' marker-line-color: white;',
614 | ' marker-placement: interior;',
615 | ' marker-clip: true;',
616 | ' marker-line-width: 1;',
617 | '}'
618 | ].join('\n')
619 | }, {
620 | description: 'should add defaults if marker symbolizer is present for layer with `::glow` modifier',
621 | input: [
622 | '#layer::glow {',
623 | ' marker-line-color: white;',
624 | '}'
625 | ].join('\n'),
626 | expected: [
627 | '#layer::glow {',
628 | ' marker-line-color: white;',
629 | ' marker-clip: true;',
630 | ' marker-line-width: 1;',
631 | '}'
632 | ].join('\n')
633 | }]
634 | };
635 |
636 | var shieldSuite = {
637 | symbolizer: 'shield',
638 | testCases: [{
639 | description: 'should add defaults if shield symbolizer is present with `shield-name` property',
640 | input: [
641 | '#layer {',
642 | ' shield-name: "wadus";',
643 | '}'
644 | ].join('\n'),
645 | expected: [
646 | '#layer {',
647 | ' shield-name: "wadus";',
648 | ' shield-clip: true;',
649 | '}'
650 | ].join('\n')
651 | }, {
652 | description: 'should add defaults if shield symbolizer is present with `shield-size` property',
653 | input: [
654 | '#layer {',
655 | ' shield-size: 20;',
656 | '}'
657 | ].join('\n'),
658 | expected: [
659 | '#layer {',
660 | ' shield-size: 20;',
661 | ' shield-clip: true;',
662 | '}'
663 | ].join('\n')
664 | }, {
665 | description: 'should not add `shield-clip` default if shield symbolizer is present and `shield-clip` is already set to true',
666 | not: true,
667 | input: [
668 | '#layer {',
669 | ' shield-size: 20;',
670 | ' shield-clip: true;',
671 | '}'
672 | ].join('\n'),
673 | expected: [
674 | '#layer {',
675 | ' shield-size: 20;',
676 | ' shield-clip: true;',
677 | '}'
678 | ].join('\n')
679 | }, {
680 | description: 'should not add `shield-clip` default if shield symbolizer is present and `shield-clip` is already set to false',
681 | input: [
682 | '#layer {',
683 | ' shield-clip: false;',
684 | '}'
685 | ].join('\n'),
686 | expected: [
687 | '#layer {',
688 | ' shield-clip: false;',
689 | '}'
690 | ].join('\n')
691 | }, {
692 | description: 'should add defaults if shield symbolizer is present in two different rules',
693 | input: [
694 | '#layer {',
695 | ' shield-name: "wadus";',
696 | '}',
697 | '#layer {',
698 | ' shield-size: 20;',
699 | '}'
700 | ].join('\n'),
701 | expected: [
702 | '#layer {',
703 | ' shield-name: "wadus";',
704 | ' shield-clip: true;',
705 | '}',
706 | '#layer {',
707 | ' shield-size: 20;',
708 | ' shield-clip: true;',
709 | '}'
710 | ].join('\n')
711 | }, {
712 | description: 'should add defaults if shield symbolizer is present with two different properties',
713 | input: [
714 | '#layer {',
715 | ' shield-name: "wadus";',
716 | ' shield-size: 20;',
717 | '}'
718 | ].join('\n'),
719 | expected: [
720 | '#layer {',
721 | ' shield-name: "wadus";',
722 | ' shield-size: 20;',
723 | ' shield-clip: true;',
724 | '}'
725 | ].join('\n')
726 | }, {
727 | description: 'should add defaults if shield symbolizer is present for layer with `::glow` modifier',
728 | input: [
729 | '#layer::glow {',
730 | ' shield-name: "wadus";',
731 | '}'
732 | ].join('\n'),
733 | expected: [
734 | '#layer::glow {',
735 | ' shield-name: "wadus";',
736 | ' shield-clip: true;',
737 | '}'
738 | ].join('\n')
739 | }]
740 | };
741 |
742 | var textSuite = {
743 | symbolizer: 'text',
744 | testCases: [{
745 | description: 'should add defaults if text symbolizer is present with `text-spacing` property',
746 | input: [
747 | '#layer {',
748 | ' text-spacing: 5;',
749 | '}'
750 | ].join('\n'),
751 | expected: [
752 | '#layer {',
753 | ' text-spacing: 5;',
754 | ' text-clip: true;',
755 | ' text-label-position-tolerance: 0;',
756 | '}'
757 | ].join('\n')
758 | }, {
759 | description: 'should add defaults if text symbolizer is present with `text-halo-fill` property',
760 | input: [
761 | '#layer {',
762 | ' text-halo-fill: #cf3;',
763 | '}'
764 | ].join('\n'),
765 | expected: [
766 | '#layer {',
767 | ' text-halo-fill: #cf3;',
768 | ' text-clip: true;',
769 | ' text-label-position-tolerance: 0;',
770 | '}'
771 | ].join('\n')
772 | }, {
773 | description: 'should not add `text-clip` default if text symbolizer is present and `text-clip` is already set to true',
774 | input: [
775 | '#layer {',
776 | ' text-halo-fill: #cf3;',
777 | ' text-clip: true;',
778 | '}'
779 | ].join('\n'),
780 | expected: [
781 | '#layer {',
782 | ' text-halo-fill: #cf3;',
783 | ' text-clip: true;',
784 | ' text-label-position-tolerance: 0;',
785 | '}'
786 | ].join('\n')
787 | }, {
788 | description: 'should not add `text-clip` default if text symbolizer is present and `text-clip` is already set to false',
789 | input: [
790 | '#layer {',
791 | ' text-clip: false;',
792 | '}'
793 | ].join('\n'),
794 | expected: [
795 | '#layer {',
796 | ' text-clip: false;',
797 | ' text-label-position-tolerance: 0;',
798 | '}'
799 | ].join('\n')
800 | }, {
801 | description: 'should not add `text-label-position-tolerance` default if text symbolizer is present and `text-label-position-tolerance` is already set to any value',
802 | input: [
803 | '#layer {',
804 | ' text-halo-fill: #cf3;',
805 | ' text-label-position-tolerance: 12.0;',
806 | '}'
807 | ].join('\n'),
808 | expected: [
809 | '#layer {',
810 | ' text-halo-fill: #cf3;',
811 | ' text-label-position-tolerance: 12.0;',
812 | ' text-clip: true;',
813 | '}'
814 | ].join('\n')
815 | }, {
816 | description: 'should add defaults if text symbolizer is present in two different rules',
817 | input: [
818 | '#layer {',
819 | ' text-spacing: 5;',
820 | '}',
821 | '#layer {',
822 | ' text-halo-fill: #cf3;',
823 | '}'
824 | ].join('\n'),
825 | expected: [
826 | '#layer {',
827 | ' text-spacing: 5;',
828 | ' text-clip: true;',
829 | ' text-label-position-tolerance: 0;',
830 | '}',
831 | '#layer {',
832 | ' text-halo-fill: #cf3;',
833 | ' text-clip: true;',
834 | ' text-label-position-tolerance: 0;',
835 | '}'
836 | ].join('\n')
837 | }, {
838 | description: 'should add defaults if text symbolizer is present with two different properties',
839 | input: [
840 | '#layer {',
841 | ' text-spacing: 5;',
842 | ' text-halo-fill: #cf3;',
843 | '}'
844 | ].join('\n'),
845 | expected: [
846 | '#layer {',
847 | ' text-spacing: 5;',
848 | ' text-halo-fill: #cf3;',
849 | ' text-clip: true;',
850 | ' text-label-position-tolerance: 0;',
851 | '}'
852 | ].join('\n')
853 | }, {
854 | description: 'should add defaults if text symbolizer is present for layer with `::glow` modifier',
855 | input: [
856 | '#layer::glow {',
857 | ' text-spacing: 5;',
858 | '}'
859 | ].join('\n'),
860 | expected: [
861 | '#layer::glow {',
862 | ' text-spacing: 5;',
863 | ' text-clip: true;',
864 | ' text-label-position-tolerance: 0;',
865 | '}'
866 | ].join('\n')
867 | }]
868 | };
869 |
870 | var urlSuite = {
871 | symbolizer: 'line-pattern',
872 | testCases: [{
873 | description: 'should handle url enclosed by simple quotes',
874 | input: [
875 | '#layer {',
876 | ' line-width: 5;',
877 | " line-pattern-file: url('https://s3.amazonaws.com/com.cartodb.users-assets.production/production/stephaniemongon/assets/20150923010945images-1.jpg');",
878 | '}'
879 | ].join('\n'),
880 | expected: [
881 | '#layer {',
882 | ' line-width: 5;',
883 | " line-pattern-file: url('https://s3.amazonaws.com/com.cartodb.users-assets.production/production/stephaniemongon/assets/20150923010945images-1.jpg');",
884 | ' line-clip: true;',
885 | ' line-pattern-clip: true;',
886 | '}'
887 | ].join('\n')
888 | }, {
889 | description: 'should handle url enclosed by double quotes',
890 | input: [
891 | '#layer {',
892 | ' line-width: 5;',
893 | ' line-pattern-file: url("https://s3.amazonaws.com/com.cartodb.users-assets.production/production/stephaniemongon/assets/20150923010945images-1.jpg");',
894 | '}'
895 | ].join('\n'),
896 | expected: [
897 | '#layer {',
898 | ' line-width: 5;',
899 | ' line-pattern-file: url("https://s3.amazonaws.com/com.cartodb.users-assets.production/production/stephaniemongon/assets/20150923010945images-1.jpg");',
900 | ' line-clip: true;',
901 | ' line-pattern-clip: true;',
902 | '}'
903 | ].join('\n')
904 | }, {
905 | description: 'should handle url w/o quotes',
906 | input: [
907 | '#layer {',
908 | ' line-width: 5;',
909 | ' line-pattern-file: url(https://s3.amazonaws.com/com.cartodb.users-assets.production/production/stephaniemongon/assets/20150923010945images-1.jpg);',
910 | '}'
911 | ].join('\n'),
912 | expected: [
913 | '#layer {',
914 | ' line-width: 5;',
915 | ' line-pattern-file: url(https://s3.amazonaws.com/com.cartodb.users-assets.production/production/stephaniemongon/assets/20150923010945images-1.jpg);',
916 | ' line-clip: true;',
917 | ' line-pattern-clip: true;',
918 | '}'
919 | ].join('\n')
920 | }, {
921 | description: 'should handle url w/o quotes and comments',
922 | input: [
923 | '#layer {',
924 | ' line-width: 5;',
925 | ' line-pattern-file: url(https://s3.amazonaws.com/com.cartodb.users-assets.production/production/stephaniemongon/assets/20150923010945images-1.jpg);//https://s3.amazonaws.com',
926 | '}'
927 | ].join('\n'),
928 | expected: [
929 | '#layer {',
930 | ' line-width: 5;',
931 | ' line-pattern-file: url(https://s3.amazonaws.com/com.cartodb.users-assets.production/production/stephaniemongon/assets/20150923010945images-1.jpg);',
932 | ' line-clip: true;',
933 | ' line-pattern-clip: true;',
934 | '}'
935 | ].join('\n')
936 | }]
937 | };
938 |
939 | var suites = []
940 | .concat(polygonSuite)
941 | .concat(polygonPatternSuite)
942 | .concat(lineSuite)
943 | .concat(linePatternSuite)
944 | .concat(markerSuite)
945 | .concat(shieldSuite)
946 | .concat(textSuite)
947 | .concat(urlSuite);
948 |
949 | suites.forEach(function (suite) {
950 | describe('for ' + suite.symbolizer + ' symbolizer', function () {
951 | suite.testCases.forEach(function (testCase) {
952 | it(testCase.description, function () {
953 | var outputStyle = this.styleTrans.transform(testCase.input, '2.3.0', '3.0.12');
954 | assert.equal(outputStyle, testCase.expected);
955 | });
956 | });
957 | });
958 | });
959 |
960 | describe('real scenarios', function () {
961 | var realScenarios = [{
962 | description: 'should set defaults for rules that contains symbolyzers',
963 | input: [
964 | '#countries {',
965 | ' ::outline {',
966 | ' line-color: #85c5d3;',
967 | ' line-width: 2;',
968 | ' line-join: round;',
969 | ' }',
970 | ' [GEOUNIT != "United States of America"]{',
971 | ' polygon-fill: #fff;',
972 | ' }',
973 | '}'
974 | ].join('\n'),
975 | expected: [
976 | '#countries {',
977 | ' ::outline {',
978 | ' line-color: #85c5d3;',
979 | ' line-width: 2;',
980 | ' line-join: round;',
981 | ' line-clip: true;',
982 | ' }',
983 | ' [GEOUNIT != "United States of America"]{',
984 | ' polygon-fill: #fff;',
985 | ' polygon-clip: true;',
986 | ' }',
987 | '}'
988 | ].join('\n')
989 | }, {
990 | description: 'should set defaults to road example',
991 | input: [
992 | '#road {',
993 | ' [class="motorway"] {',
994 | ' ::case {',
995 | ' line-width: 5;',
996 | ' line-color: #d83;',
997 | ' }',
998 | ' ::fill {',
999 | ' line-width: 2.5;',
1000 | ' line-color: #fe3;',
1001 | ' }',
1002 | ' }',
1003 | ' [class="main"] {',
1004 | ' ::case {',
1005 | ' line-width: 4.5;',
1006 | ' line-color: #ca8;',
1007 | ' }',
1008 | ' ::fill {',
1009 | ' line-width: 2;',
1010 | ' line-color: #ffa;',
1011 | ' }',
1012 | ' }',
1013 | '}'
1014 | ].join('\n'),
1015 | expected: [
1016 | '#road {',
1017 | ' [class="motorway"] {',
1018 | ' ::case {',
1019 | ' line-width: 5;',
1020 | ' line-color: #d83;',
1021 | ' line-clip: true;',
1022 | ' }',
1023 | ' ::fill {',
1024 | ' line-width: 2.5;',
1025 | ' line-color: #fe3;',
1026 | ' line-clip: true;',
1027 | ' }',
1028 | ' }',
1029 | ' [class="main"] {',
1030 | ' ::case {',
1031 | ' line-width: 4.5;',
1032 | ' line-color: #ca8;',
1033 | ' line-clip: true;',
1034 | ' }',
1035 | ' ::fill {',
1036 | ' line-width: 2;',
1037 | ' line-color: #ffa;',
1038 | ' line-clip: true;',
1039 | ' }',
1040 | ' }',
1041 | '}'
1042 | ].join('\n')
1043 | }, {
1044 | description: 'should accept multiline comments: "/* ... */"',
1045 | input: [
1046 | '#road {',
1047 | ' /* [class="railway"] {',
1048 | ' ::glow { */',
1049 | ' [class="motorway"] {',
1050 | ' ::case {',
1051 | ' line-width: 5; /* line-width: 10; */',
1052 | ' line-color: #d83;',
1053 | ' }',
1054 | ' }',
1055 | '}'
1056 | ].join('\n'),
1057 | expected: [
1058 | '#road {',
1059 | ' /* [class="railway"] {',
1060 | ' ::glow { */',
1061 | ' [class="motorway"] {',
1062 | ' ::case {',
1063 | ' line-width: 5; /* line-width: 10; */',
1064 | ' line-color: #d83;',
1065 | ' line-clip: true;',
1066 | ' }',
1067 | ' }',
1068 | '}'
1069 | ].join('\n')
1070 | }, {
1071 | description: 'should accept one line comments: "// ..."',
1072 | input: [
1073 | '#road {',
1074 | ' // [class="railway"] {',
1075 | ' [class="motorway"] {',
1076 | ' ::case {',
1077 | ' line-width: 5;',
1078 | ' line-color: #d83;// line-color: #cf3;',
1079 | ' }',
1080 | ' }',
1081 | '}'
1082 | ].join('\n'),
1083 | expected: [
1084 | '#road {',
1085 | ' [class="motorway"] {',
1086 | ' ::case {',
1087 | ' line-width: 5;',
1088 | ' line-color: #d83;',
1089 | ' line-clip: true;',
1090 | ' }',
1091 | ' }',
1092 | '}'
1093 | ].join('\n')
1094 | }, {
1095 | description: 'should accept column atributtes',
1096 | input: [
1097 | 'Map {',
1098 | ' buffer-size: 256;',
1099 | '}',
1100 | '#county_points_with_population {',
1101 | ' marker-fill-opacity: 0.1;',
1102 | ' marker-line-color:#FFFFFF;//#CF1C90;',
1103 | ' marker-line-width: 0;',
1104 | ' marker-line-opacity: 0.3;',
1105 | ' marker-placement: point;',
1106 | ' marker-type: ellipse;',
1107 | ' marker-width: [cartodb_id];',
1108 | ' [zoom=5]{marker-width: [cartodb_id]*2;}',
1109 | ' [zoom=6]{marker-width: [cartodb_id]*4;}',
1110 | ' marker-fill: #000000;',
1111 | ' marker-allow-overlap: true;',
1112 | '}'
1113 | ].join('\n'),
1114 | expected: [
1115 | 'Map {',
1116 | ' buffer-size: 256;',
1117 | '}',
1118 | '#county_points_with_population {',
1119 | ' marker-fill-opacity: 0.1;',
1120 | ' marker-line-color:#FFFFFF;',
1121 | ' marker-line-width: 0;',
1122 | ' marker-line-opacity: 0.3;',
1123 | ' marker-placement: point;',
1124 | ' marker-type: ellipse;',
1125 | ' marker-width: [cartodb_id];',
1126 | ' [zoom=5]{marker-width: [cartodb_id]*2;}',
1127 | ' [zoom=6]{marker-width: [cartodb_id]*4;}',
1128 | ' marker-fill: #000000;',
1129 | ' marker-allow-overlap: true;',
1130 | ' marker-clip: true;',
1131 | '}'
1132 | ].join('\n')
1133 | }, {
1134 | description: 'should add defaults for turbo-cartocss outputs',
1135 | input: [
1136 | '#points {',
1137 | ' marker-fill: #fee5d9;',
1138 | ' [ scalerank = 6 ] {',
1139 | ' marker-fill: #fcae91',
1140 | ' }',
1141 | ' [ scalerank = 8 ] {',
1142 | ' marker-fill: #fb6a4a',
1143 | ' }',
1144 | ' [ scalerank = 4 ] {',
1145 | ' marker-fill: #de2d26',
1146 | ' }',
1147 | ' [ scalerank = 10 ] {',
1148 | ' marker-fill: #a50f15',
1149 | ' }',
1150 | '}'
1151 | ].join('\n'),
1152 | expected: [
1153 | '#points {',
1154 | ' marker-fill: #fee5d9;',
1155 | ' [ scalerank = 6 ] {',
1156 | ' marker-fill: #fcae91',
1157 | ' }',
1158 | ' [ scalerank = 8 ] {',
1159 | ' marker-fill: #fb6a4a',
1160 | ' }',
1161 | ' [ scalerank = 4 ] {',
1162 | ' marker-fill: #de2d26',
1163 | ' }',
1164 | ' [ scalerank = 10 ] {',
1165 | ' marker-fill: #a50f15',
1166 | ' }',
1167 | ' marker-clip: true',
1168 | '}'
1169 | ].join('\n')
1170 | }, { // see: https://github.com/CartoDB/grainstore/issues/136
1171 | description: 'should not add defaults when parent has symbolizer already defined',
1172 | input: [
1173 | '#layer {',
1174 | ' marker-width: 4;',
1175 | ' [ pop_max > 10000 ] {',
1176 | ' marker-width: 8;',
1177 | ' }',
1178 | ' [ pop_max > 100000 ] {',
1179 | ' marker-width: 16;',
1180 | ' }',
1181 | '}'
1182 | ].join('\n'),
1183 | expected: [
1184 | '#layer {',
1185 | ' marker-width: 4;',
1186 | ' [ pop_max > 10000 ] {',
1187 | ' marker-width: 8;',
1188 | ' }',
1189 | ' [ pop_max > 100000 ] {',
1190 | ' marker-width: 16;',
1191 | ' }',
1192 | ' marker-clip: true',
1193 | '}'
1194 | ].join('\n')
1195 | }, {
1196 | description: 'should not add defaults when all parents have the symbolizer already defined',
1197 | input: [
1198 | '#layer {',
1199 | ' marker-width: 4;',
1200 | ' marker-clip: false;',
1201 | ' [pop_max > 0] {',
1202 | ' marker-clip: true;',
1203 | ' marker-width: 8;',
1204 | ' [pop_max > 100] {',
1205 | ' marker-width: 16;',
1206 | ' }',
1207 | ' }',
1208 | '}'
1209 | ].join('\n'),
1210 | expected: [
1211 | '#layer {',
1212 | ' marker-width: 4;',
1213 | ' marker-clip: false;',
1214 | ' [pop_max > 0] {',
1215 | ' marker-clip: true;',
1216 | ' marker-width: 8;',
1217 | ' [pop_max > 100] {',
1218 | ' marker-width: 16;',
1219 | ' }',
1220 | ' }',
1221 | '}'
1222 | ].join('\n')
1223 | }, {
1224 | description: 'should just add defaults to the root rule',
1225 | input: [
1226 | '#layer {',
1227 | ' marker-width: 4;',
1228 | ' [pop_max > 0] {',
1229 | ' marker-width: 8;',
1230 | ' [pop_max > 100] {',
1231 | ' marker-width: 16;',
1232 | ' }',
1233 | ' }',
1234 | '}'
1235 | ].join('\n'),
1236 | expected: [
1237 | '#layer {',
1238 | ' marker-width: 4;',
1239 | ' [pop_max > 0] {',
1240 | ' marker-width: 8;',
1241 | ' [pop_max > 100] {',
1242 | ' marker-width: 16;',
1243 | ' }',
1244 | ' }',
1245 | ' marker-clip: true',
1246 | '}'
1247 | ].join('\n')
1248 | }, {
1249 | description: 'should just add defaults to the parent rule but to the children rule',
1250 | input: [
1251 | '#layer {',
1252 | ' marker-width: 4;',
1253 | ' [pop_max > 0] {',
1254 | ' marker-width: 8;',
1255 | ' marker-clip: true;',
1256 | ' [pop_max > 100] {',
1257 | ' marker-width: 16;',
1258 | ' }',
1259 | ' }',
1260 | '}'
1261 | ].join('\n'),
1262 | expected: [
1263 | '#layer {',
1264 | ' marker-width: 4;',
1265 | ' [pop_max > 0] {',
1266 | ' marker-width: 8;',
1267 | ' marker-clip: true;',
1268 | ' [pop_max > 100] {',
1269 | ' marker-width: 16;',
1270 | ' }',
1271 | ' }',
1272 | ' marker-clip: true',
1273 | '}'
1274 | ].join('\n')
1275 | }];
1276 |
1277 | realScenarios.forEach(function (scenario) {
1278 | it(scenario.description, function () {
1279 | var output = this.styleTrans.transform(scenario.input, '2.3.0', '3.0.12');
1280 | assert.equal(output, scenario.expected);
1281 | });
1282 | });
1283 | });
1284 | });
1285 |
--------------------------------------------------------------------------------
/test/mml_builder.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var assert = require('assert');
4 | var _ = require('underscore');
5 | var grainstore = require('../lib/grainstore');
6 | var step = require('step');
7 | var http = require('http');
8 | var fs = require('fs');
9 | var carto = require('carto');
10 | var semver = require('semver');
11 |
12 | const xml2js = require('xml2js');
13 | const xpath = require('xml2js-xpath');
14 |
15 | var server;
16 |
17 | var serverPort = 8033;
18 |
19 | var DEFAULT_POINT_STYLE = [
20 | '#layer {',
21 | ' marker-fill: #FF6600;',
22 | ' marker-opacity: 1;',
23 | ' marker-width: 16;',
24 | ' marker-line-color: white;',
25 | ' marker-line-width: 3;',
26 | ' marker-line-opacity: 0.9;',
27 | ' marker-placement: point;',
28 | ' marker-type: ellipse;',
29 | ' marker-allow-overlap: true;',
30 | '}'
31 | ].join('');
32 |
33 | var SAMPLE_SQL = 'SELECT ST_MakePoint(0,0)';
34 |
35 | [false, true].forEach(function (useWorkers) {
36 | describe('mmlBuilder use_workers=' + useWorkers, function () {
37 | before(function (done) {
38 | // Start a server to test external resources
39 | server = http.createServer(function (request, response) {
40 | var filename = 'test/support/resources' + request.url;
41 | fs.readFile(filename, 'binary', function (err, file) {
42 | if (err) {
43 | response.writeHead(404, { 'Content-Type': 'text/plain' });
44 | console.log("File '" + filename + "' not found");
45 | response.write('404 Not Found\n');
46 | } else {
47 | response.writeHead(200);
48 | response.write(file, 'binary');
49 | }
50 | response.end();
51 | });
52 | });
53 | server.listen(serverPort, done);
54 | });
55 |
56 | after(function () {
57 | server.close();
58 | });
59 |
60 | it('can generate base mml with normal ops', function (done) {
61 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
62 | var mmlBuilder = mmlStore.mml_builder({ dbname: 'my_database', sql: SAMPLE_SQL, style: DEFAULT_POINT_STYLE });
63 | var baseMML = mmlBuilder.baseMML();
64 |
65 | assert.ok(_.isArray(baseMML.Layer));
66 | assert.equal(baseMML.Layer[0].id, 'layer0');
67 | assert.equal(baseMML.Layer[0].Datasource.dbname, 'my_database');
68 |
69 | done();
70 | });
71 |
72 | it('can be initialized with custom style and version', function (done) {
73 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers, mapnik_version: '2.1.0' });
74 | mmlStore.mml_builder({ dbname: 'd', sql: SAMPLE_SQL, style: DEFAULT_POINT_STYLE, styleVersion: '2.0.2' })
75 | .toXML(done);
76 | });
77 |
78 | it('can be initialized with custom interactivity', function (done) {
79 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers, mapnik_version: '2.1.0' });
80 | mmlStore.mml_builder({ dbname: 'd', sql: SAMPLE_SQL, style: DEFAULT_POINT_STYLE, interactivity: 'cartodb_id' })
81 | .toXML(done);
82 | });
83 |
84 | it('can generate base mml with overridden authentication', function (done) {
85 | var mmlStore = new grainstore.MMLStore({
86 | use_workers: useWorkers,
87 | datasource: {
88 | user: 'overridden_user',
89 | password: 'overridden_password'
90 | }
91 | }
92 | );
93 | var mmlBuilder = mmlStore.mml_builder({
94 | dbname: 'my_database',
95 | sql: SAMPLE_SQL,
96 | style: DEFAULT_POINT_STYLE,
97 | // NOTE: authentication tokens here are silently discarded
98 | user: 'shadow_user',
99 | password: 'shadow_password'
100 | });
101 | var baseMML = mmlBuilder.baseMML();
102 |
103 | assert.ok(_.isArray(baseMML.Layer));
104 | assert.equal(baseMML.Layer[0].id, 'layer0');
105 | assert.equal(baseMML.Layer[0].Datasource.dbname, 'my_database');
106 | assert.equal(baseMML.Layer[0].Datasource.user, 'overridden_user');
107 | assert.equal(baseMML.Layer[0].Datasource.password, 'overridden_password');
108 |
109 | done();
110 | });
111 |
112 | it('search_path is set in the datasource', function (done) {
113 | var searchPath = "'foo', 'bar'";
114 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
115 | var mmlBuilder = mmlStore.mml_builder(
116 | { dbname: 'my_database', sql: SAMPLE_SQL, style: DEFAULT_POINT_STYLE, search_path: searchPath }
117 | );
118 |
119 | var baseMML = mmlBuilder.baseMML();
120 | assert.equal(baseMML.Layer[0].Datasource.search_path, searchPath);
121 | done();
122 | });
123 |
124 | it('search_path is NOT set in the datasource', function (done) {
125 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
126 | var mmlBuilder = mmlStore.mml_builder(
127 | { dbname: 'my_database', sql: SAMPLE_SQL, style: DEFAULT_POINT_STYLE, search_path: null }
128 | );
129 | var baseMML = mmlBuilder.baseMML();
130 | assert.ok(
131 | !Object.prototype.hasOwnProperty.call(baseMML.Layer[0].Datasource, 'search_path'),
132 | 'search_path was not expected in the datasource but was found with value: ' +
133 | baseMML.Layer[0].Datasource.search_path
134 | );
135 | done();
136 | });
137 |
138 | it('default format is png', function (done) {
139 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
140 | var mmlBuilder = mmlStore.mml_builder({ dbname: 'my_database', sql: SAMPLE_SQL, style: DEFAULT_POINT_STYLE });
141 | var baseMML = mmlBuilder.baseMML();
142 | assert.equal(baseMML.format, 'png');
143 | done();
144 | });
145 |
146 | it('format can be overwritten with optional args', function (done) {
147 | var format = 'png32';
148 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers, mapnik_tile_format: format });
149 | var mmlBuilder = mmlStore.mml_builder({ dbname: 'my_database', sql: SAMPLE_SQL, style: DEFAULT_POINT_STYLE });
150 | var baseMML = mmlBuilder.baseMML();
151 | assert.equal(baseMML.format, format);
152 | done();
153 | });
154 |
155 | it('can override authentication with mmlBuilder constructor', function (done) {
156 | var mmlStore = new grainstore.MMLStore({
157 | use_workers: useWorkers,
158 | datasource: { user: 'shadow_user', password: 'shadow_password' }
159 | });
160 | var mmlBuilder = mmlStore.mml_builder({
161 | dbname: 'my_database',
162 | sql: SAMPLE_SQL,
163 | style: DEFAULT_POINT_STYLE,
164 | dbuser: 'overridden_user',
165 | dbpassword: 'overridden_password'
166 | }
167 | );
168 |
169 | var baseMML = mmlBuilder.baseMML();
170 |
171 | assert.ok(_.isArray(baseMML.Layer));
172 | assert.equal(baseMML.Layer[0].id, 'layer0');
173 | assert.equal(baseMML.Layer[0].Datasource.dbname, 'my_database');
174 | assert.equal(baseMML.Layer[0].Datasource.user, 'overridden_user');
175 | assert.equal(baseMML.Layer[0].Datasource.password, 'overridden_password');
176 |
177 | // Test that new mmlBuilder, with no overridden user/password, uses the default ones
178 | var mmlBuilder2 = mmlStore.mml_builder({ dbname: 'my_database', sql: SAMPLE_SQL, style: DEFAULT_POINT_STYLE });
179 | var baseMML2 = mmlBuilder2.baseMML();
180 | assert.equal(baseMML2.Layer[0].id, 'layer0');
181 | assert.equal(baseMML2.Layer[0].Datasource.dbname, 'my_database');
182 | assert.equal(baseMML2.Layer[0].Datasource.user, 'shadow_user');
183 | assert.equal(baseMML2.Layer[0].Datasource.password, 'shadow_password');
184 |
185 | done();
186 | });
187 |
188 | // See https://github.com/CartoDB/grainstore/issues/70
189 | it('can override db host and port with mmlBuilder constructor', function (done) {
190 | var mmlStore = new grainstore.MMLStore({
191 | use_workers: useWorkers,
192 | datasource: { host: 'shadow_host', port: 'shadow_port' }
193 | });
194 | var mmlBuilder = mmlStore.mml_builder({
195 | dbname: 'my_database',
196 | sql: SAMPLE_SQL,
197 | style: DEFAULT_POINT_STYLE,
198 | dbhost: 'overridden_host',
199 | dbport: 'overridden_port'
200 | }
201 | );
202 |
203 | var baseMML = mmlBuilder.baseMML();
204 |
205 | assert.ok(_.isArray(baseMML.Layer));
206 | assert.equal(baseMML.Layer[0].id, 'layer0');
207 | assert.equal(baseMML.Layer[0].Datasource.dbname, 'my_database');
208 | assert.equal(baseMML.Layer[0].Datasource.host, 'overridden_host');
209 | assert.equal(baseMML.Layer[0].Datasource.port, 'overridden_port');
210 |
211 | // Test that new mmlBuilder, with no overridden user/password, uses the default ones
212 | var mmlBuilder2 = mmlStore.mml_builder({ dbname: 'my_database', sql: SAMPLE_SQL, style: DEFAULT_POINT_STYLE });
213 | var baseMML2 = mmlBuilder2.baseMML();
214 | assert.equal(baseMML2.Layer[0].id, 'layer0');
215 | assert.equal(baseMML2.Layer[0].Datasource.dbname, 'my_database');
216 | assert.equal(baseMML2.Layer[0].Datasource.host, 'shadow_host');
217 | assert.equal(baseMML2.Layer[0].Datasource.port, 'shadow_port');
218 |
219 | done();
220 | });
221 |
222 | it('can generate base mml with sql ops, maintain id', function (done) {
223 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
224 | var mmlBuilder = mmlStore.mml_builder(
225 | { dbname: 'my_database', sql: 'SELECT * from my_table', style: DEFAULT_POINT_STYLE }
226 | );
227 | var baseMML = mmlBuilder.baseMML();
228 | assert.equal(baseMML.Layer[0].id, 'layer0');
229 | assert.equal(baseMML.Layer[0].Datasource.table, 'SELECT * from my_table');
230 | done();
231 | });
232 |
233 | it('can force plain base mml with sql ops', function (done) {
234 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
235 | var mmlBuilder = mmlStore.mml_builder(
236 | { dbname: 'my_database', sql: SAMPLE_SQL, style: DEFAULT_POINT_STYLE }
237 | );
238 | var baseMML = mmlBuilder.baseMML();
239 | assert.equal(baseMML.Layer[0].id, 'layer0');
240 | assert.equal(baseMML.Layer[0].Datasource.table, SAMPLE_SQL);
241 | done();
242 | });
243 |
244 | it('can generate full mml with style', function (done) {
245 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
246 | var mmlBuilder = mmlStore.mml_builder({ dbname: 'my_database', sql: SAMPLE_SQL, style: DEFAULT_POINT_STYLE });
247 | var mml = mmlBuilder.toMML('my carto style');
248 | assert.equal(mml.Stylesheet[0].data, 'my carto style');
249 | done();
250 | });
251 |
252 | it('can render XML from full mml with style', function (done) {
253 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
254 | var mmlBuilder = mmlStore.mml_builder(
255 | { dbname: 'my_database', sql: 'my_table', style: '#my_table {\n polygon-fill: #fff;\n}' }
256 | );
257 | mmlBuilder.toXML(function (err, output) {
258 | assert.ok(_.isNull(err), _.isNull(err) ? '' : err.message);
259 | assert.ok(output);
260 | done();
261 | });
262 | });
263 |
264 | it('Render a 2.2.0 style', function (done) {
265 | var style = '#t { polygon-fill: #fff; }';
266 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers, mapnik_version: '2.2.0' });
267 | mmlStore.mml_builder({ dbname: 'd', sql: SAMPLE_SQL, style: style }).toXML(function (err, output) {
268 | try {
269 | assert.ok(_.isNull(err), _.isNull(err) ? '' : err.message);
270 | assert.ok(output);
271 | xml2js.parseString(output, (err, xmlDoc) => {
272 | if (err) { done(err); return; }
273 | const srs = xpath.find(xmlDoc, '//PolygonSymbolizer/@fill');
274 | assert.equal(srs[0].$.fill, '#ffffff');
275 | done();
276 | });
277 | } catch (err) { done(err); }
278 | });
279 | });
280 |
281 | it('can render errors from full mml with bad style', function (done) {
282 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
283 | mmlStore.mml_builder(
284 | { dbname: 'my_database', sql: SAMPLE_SQL, style: '#my_table {\n backgrxxxxxound-color: #fff;\n}' }
285 | ).toXML(function (err) {
286 | assert.ok(err.message.match(/Unrecognized rule/), err.message);
287 | done();
288 | });
289 | });
290 |
291 | it('can render multiple errors from full mml with bad style', function (done) {
292 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
293 | mmlStore.mml_builder(
294 | { dbname: 'my_database', sql: SAMPLE_SQL, style: '#my_table {\n backgrxxound-color: #fff;bad-tag: #fff;\n}' }
295 | ).toXML(
296 | function (err) {
297 | assert.ok(err.message.match(/Unrecognized rule[\s\S]*Unrecognized rule/), err.message);
298 | done();
299 | }
300 | );
301 | });
302 |
303 | it('retrieves a dynamic style should return XML with dynamic style', function (done) {
304 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
305 | mmlStore.mml_builder({ dbname: 'my_databaasez', sql: 'my_tablez', style: '#my_tablez {marker-fill: #000000;}' })
306 | .toXML(function (err, data) {
307 | if (err) { return done(err); }
308 | xml2js.parseString(data, (err, xmlDoc) => {
309 | if (err) { done(err); return; }
310 | const color = xpath.find(xmlDoc, '//MarkersSymbolizer/@fill')[0];
311 | assert.equal(color.$.fill, '#000000');
312 | done();
313 | });
314 | });
315 | });
316 |
317 | it('includes interactivity in XML', function (done) {
318 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
319 | mmlStore.mml_builder(
320 | {
321 | dbname: 'd2',
322 | sql: SAMPLE_SQL,
323 | style: DEFAULT_POINT_STYLE,
324 | interactivity: 'a,b'
325 | }).toXML(function (err, data) {
326 | if (err) { return done(err); }
327 | xml2js.parseString(data, (err, xmlDoc) => {
328 | if (err) { done(err); return; }
329 |
330 | const layer = xpath.find(xmlDoc, "//Parameter[@name='interactivity_layer']")[0];
331 | assert.equal(layer._, 'layer0');
332 |
333 | const fields = xpath.find(xmlDoc, "//Parameter[@name='interactivity_fields']")[0];
334 | assert.equal(fields._, 'a,b');
335 | done();
336 | });
337 | });
338 | });
339 |
340 | // See https://github.com/Vizzuality/grainstore/issues/61
341 | it('zoom variable is special', function (done) {
342 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
343 | mmlStore.mml_builder(
344 | {
345 | dbname: 'd',
346 | sql: SAMPLE_SQL,
347 | style: '#t [ zoom >= 4 ] {marker-fill:red;}'
348 | }).toXML(function (err, data) {
349 | if (err) { return done(err); }
350 | xml2js.parseString(data, (err, xmlDoc) => {
351 | if (err) { done(err); return; }
352 |
353 | const x = xpath.find(xmlDoc, '//MaxScaleDenominator')[0];
354 | assert.ok(x, "Xpath '//MaxScaleDenominator' does not match " + xmlDoc);
355 | assert.equal(x, '50000000');
356 |
357 | done();
358 | });
359 | });
360 | });
361 |
362 | it('quotes in CartoCSS are accepted', function (done) {
363 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
364 | mmlStore.mml_builder(
365 | {
366 | dbname: 'd',
367 | table: 't',
368 | sql: ["select 'x' as n, 'SRID=3857;POINT(0 0)'::geometry as the_geom_webmercator",
369 | "select 'x' as n, 'SRID=3857;POINT(2 0)'::geometry as the_geom_webmercator"],
370 | style: ['#t [n="t\'q"] {marker-fill:red;}', '#t[n=\'t"q\'] {marker-fill:green;}']
371 | }).toXML(function (err, data) {
372 | if (err) { return done(err); }
373 | xml2js.parseString(data, (err, xmlDoc) => {
374 | if (err) { done(err); return; }
375 |
376 | const x = xpath.find(xmlDoc, '//Filter');
377 | assert.equal(x.length, 2);
378 |
379 | for (var i = 0; i < 2; ++i) {
380 | var f = x[i];
381 | var m = f.toString().match(/(['"])t(\\?)(["'])q(['"])/);
382 | assert.ok(m, 'Unexpected filter: ' + f.toString());
383 | assert.equal(m[1], m[4]); // opening an closing quotes are the same
384 | // internal quote must be different or escaped
385 | assert.ok(m[3] !== m[1] || m[2] === '\\', 'Unescaped quote ' + m[3] + ' found: ' + f.toString());
386 | }
387 |
388 | done();
389 | });
390 | });
391 | });
392 |
393 | it('base style and custom style keys do not affect each other', function (done) {
394 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
395 | var style1 = '#tab { marker-fill: #111111; }';
396 | var style2 = '#tab { marker-fill: #222222; }';
397 | var style3 = '#tab { marker-fill: #333333; }';
398 | var baseBuilder = mmlStore.mml_builder({ dbname: 'db', sql: 'tab', style: style1 });
399 | var custBuilder = mmlStore.mml_builder({ dbname: 'db', sql: 'tab', style: style2 });
400 | step(
401 | function checkBase1 () {
402 | var cb = this;
403 | baseBuilder.toXML(function (err, xml) {
404 | if (err) { cb(err); return; }
405 | xml2js.parseString(xml, (err, xmlDoc) => {
406 | if (err) { cb(err); return; }
407 | const color = xpath.find(xmlDoc, '//MarkersSymbolizer/@fill')[0];
408 | assert.equal(color.$.fill, '#111111');
409 | cb(null);
410 | });
411 | });
412 | },
413 | function checkCustom1 (err) {
414 | if (err) {
415 | throw err;
416 | }
417 | var cb = this;
418 | custBuilder.toXML(function (err, xml) {
419 | if (err) { cb(err); return; }
420 | xml2js.parseString(xml, (err, xmlDoc) => {
421 | if (err) { cb(err); return; }
422 | const color = xpath.find(xmlDoc, '//MarkersSymbolizer/@fill')[0];
423 | assert.equal(color.$.fill, '#222222');
424 | cb(null);
425 | });
426 | });
427 | },
428 | function checkCustom2 (err) {
429 | if (err) {
430 | throw err;
431 | }
432 | var cb = this;
433 | mmlStore.mml_builder({ dbname: 'db', sql: 'tab', style: style3 }).toXML(function (err, xml) {
434 | if (err) { cb(err); return; }
435 | xml2js.parseString(xml, (err, xmlDoc) => {
436 | if (err) { cb(err); return; }
437 | const color = xpath.find(xmlDoc, '//MarkersSymbolizer/@fill')[0];
438 | assert.equal(color.$.fill, '#333333');
439 | done();
440 | });
441 | });
442 | }
443 | );
444 | });
445 |
446 | it('can retrieve basic XML', function (done) {
447 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
448 | mmlStore.mml_builder({ dbname: 'my_databaasez', sql: SAMPLE_SQL, style: DEFAULT_POINT_STYLE })
449 | .toXML(function (err, data) {
450 | if (err) { done(err); return; }
451 | xml2js.parseString(data, (err, xmlDoc) => {
452 | if (err) { done(err); return; }
453 | const sql = xpath.find(xmlDoc, "//Parameter[@name='table']")[0];
454 | assert.equal(sql._, SAMPLE_SQL);
455 | done();
456 | });
457 | });
458 | });
459 |
460 | it('XML contains connection parameters', function (done) {
461 | var mmlStore = new grainstore.MMLStore({
462 | use_workers: useWorkers,
463 | datasource: {
464 | user: 'u', host: 'h', port: '12', password: 'p'
465 | }
466 | });
467 | mmlStore.mml_builder({ dbname: 'd', sql: SAMPLE_SQL, style: DEFAULT_POINT_STYLE }).toXML(function (err, data) {
468 | assert.ok(data, err);
469 | xml2js.parseString(data, (err, xmlDoc) => {
470 | if (err) { done(err); return; }
471 | var node = xpath.find(xmlDoc, "//Parameter[@name='user']")[0];
472 | assert.equal(node._, 'u');
473 | node = xpath.find(xmlDoc, "//Parameter[@name='host']")[0];
474 | assert.equal(node._, 'h');
475 | node = xpath.find(xmlDoc, "//Parameter[@name='port']")[0];
476 | assert.equal(node._, '12');
477 | node = xpath.find(xmlDoc, "//Parameter[@name='password']")[0];
478 | assert.equal(node._, 'p');
479 | done();
480 | });
481 | });
482 | });
483 |
484 | it('can retrieve basic XML specifying sql', function (done) {
485 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
486 | mmlStore.mml_builder({ dbname: 'db', sql: 'SELECT * FROM my_face', style: DEFAULT_POINT_STYLE })
487 | .toXML(function (err, data) {
488 | if (err) { done(err); return; }
489 | xml2js.parseString(data, (err, xmlDoc) => {
490 | if (err) { done(err); return; }
491 | let sql = xpath.find(xmlDoc, "//Parameter[@name='table']")[0];
492 | assert.equal(sql._, 'SELECT * FROM my_face');
493 |
494 | mmlStore.mml_builder({ dbname: 'db', sql: 'tab', style: DEFAULT_POINT_STYLE }).toXML(function (err, data) {
495 | if (err) { done(err); return; }
496 | xml2js.parseString(data, (err, xmlDoc) => {
497 | if (err) { done(err); return; }
498 | sql = xpath.find(xmlDoc, "//Parameter[@name='table']")[0];
499 | assert.equal(sql._, 'tab');
500 | // NOTE: there's no need to explicitly delete style
501 | // of mmlBuilder because it is an extension
502 | // of mmlBuilder2 (extended by SQL)
503 | done();
504 | });
505 | });
506 | });
507 | });
508 | });
509 |
510 | it('by default datasource has full webmercator extent', function (done) {
511 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
512 | var mmlBuilder = mmlStore.mml_builder(
513 | { dbname: 'my_database', sql: 'SELECT * FROM my_face', style: DEFAULT_POINT_STYLE }
514 | );
515 | var baseMML = mmlBuilder.baseMML();
516 | assert.ok(_.isArray(baseMML.Layer));
517 | assert.equal(baseMML.Layer[0].Datasource.extent, '-20037508.3,-20037508.3,20037508.3,20037508.3');
518 | done();
519 | });
520 |
521 | it('SRS in XML should use the "+init=epsg:xxx" form', function (done) {
522 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
523 | mmlStore.mml_builder({ dbname: 'my_databaasez', sql: 'SELECT * FROM my_face', style: DEFAULT_POINT_STYLE })
524 | .toXML(function (err, data) {
525 | if (err) { return done(err); }
526 | xml2js.parseString(data, (err, xmlDoc) => {
527 | if (err) { done(err); return; }
528 | const srs = xpath.find(xmlDoc, '//@srs');
529 | assert.equal(srs[0].$.srs.indexOf('+init=epsg:'), 0,
530 | '"' + srs[0].$.srs + '" does not start with "+init=epsg:"');
531 | done();
532 | });
533 | });
534 | });
535 |
536 | it('store, retrive and convert to XML a set of reference styles', function (done) {
537 | var cachedir = '/tmp/gt-' + process.pid;
538 |
539 | var styles = [
540 | // point-transform without point-file
541 | {
542 | cartocss: "#tab { point-transform: 'scale(0.9)'; }",
543 | reXML: /PointSymbolizer transform="scale\(0.9\)"/
544 | },
545 | // localize external resources
546 | {
547 | cartocss: "#tab { point-file: url('http://localhost:" + serverPort + "/circle.svg'); }",
548 | reXML: new RegExp('PointSymbolizer file="' + cachedir + '/cache/.*.svg"')
549 | },
550 | // localize external resources with a + in the url
551 | {
552 | cartocss: "#tab { point-file: url('http://localhost:" + serverPort + "/+circle.svg'); }",
553 | reXML: new RegExp('PointSymbolizer file="' + cachedir + '/cache/.*.svg"')
554 | },
555 | // transform marker-width and height from 2.0.0 to 2.1.0 resources with a + in the url
556 | {
557 | cartocss: '#tab { marker-width: 8; marker-height: 3; }',
558 | version: '2.0.0',
559 | target_version: '2.1.0',
560 | reXML: new RegExp('MarkersSymbolizer width="16" height="6"')
561 | },
562 | // recognize mapnik-geometry-type
563 | {
564 | cartocss: '#tab [mapnik-geometry-type=3] { marker-placement:line; }',
565 | reXML: /Filter.*\[mapnik::geometry_type\] = 3.*Filter/
566 | },
567 | // properly encode & signs
568 | // see http://github.com/CartoDB/cartodb20/issues/137
569 | {
570 | cartocss: "#tab [f='&'] { marker-width: 8; }",
571 | reXML: /\(\[f\] = '&'\)<\/Filter>/
572 | }
573 | ];
574 |
575 | var StylesRunner = function (styles, done) {
576 | this.styles = styles;
577 | this.done = done;
578 | this.errors = [];
579 | };
580 |
581 | StylesRunner.prototype.runNext = function (err) {
582 | if (err) {
583 | this.errors.push(err);
584 | }
585 | if (!this.styles.length) {
586 | err = this.errors.length ? new Error(this.errors) : null;
587 | // TODO: remove all from cachedir ?
588 | const mmlStore = new grainstore.MMLStore({ use_workers: useWorkers, cachedir: cachedir });
589 | const that = this;
590 | mmlStore.purgeLocalizedResources(0, function (e) {
591 | if (e) {
592 | console.log('Error purging localized resources: ' + e);
593 | }
594 | that.done(err);
595 | });
596 | return;
597 | }
598 | const that = this;
599 | var styleSpec = this.styles.shift();
600 | var style = styleSpec.cartocss;
601 | var styleVersion = styleSpec.version || '2.0.2';
602 | var targetMapnikVersion = styleSpec.target_version || styleVersion;
603 | var reXML = styleSpec.reXML;
604 |
605 | const mmlStore = new grainstore.MMLStore({
606 | use_workers: useWorkers,
607 | cachedir: cachedir,
608 | mapnik_version: targetMapnikVersion,
609 | cachettl: 0.01
610 | });
611 | var mmlBuilder = mmlStore.mml_builder({ dbname: 'db', sql: 'tab', style: style });
612 | step(
613 | function toXML () {
614 | mmlBuilder.toXML(this);
615 | },
616 | function finish (err, data) {
617 | var errs = [];
618 | if (err) {
619 | errs.push(err);
620 | }
621 | // console.log("toXML returned: "); console.dir(data);
622 | assert.ok(reXML.test(data), 'toXML: ' + style + ': expected ' + reXML + ' got:\n' + data);
623 | that.runNext(err);
624 | }
625 | );
626 | };
627 |
628 | var runner = new StylesRunner(styles, done);
629 | runner.runNext();
630 | });
631 |
632 | // External resources are downloaded in isolation
633 | // See https://github.com/Vizzuality/grainstore/issues/60
634 | it('external resources are downloaded in isolation', function (done) {
635 | var style = "{ point-file: url('http://localhost:" + serverPort + "/circle.svg'); }";
636 | var cachedir = '/tmp/gt1-' + process.pid;
637 |
638 | var cdir1 = cachedir + '1';
639 | var style1 = '#t1 ' + style;
640 | var store1 = new grainstore.MMLStore({ use_workers: useWorkers, cachedir: cdir1 });
641 | var re1 = new RegExp('PointSymbolizer file="' + cdir1 + '/cache/.*.svg"');
642 |
643 | var cdir2 = cachedir + '2';
644 | var style2 = '#t2 ' + style;
645 | var store2 = new grainstore.MMLStore({ use_workers: useWorkers, cachedir: cdir2 });
646 | var re2 = new RegExp('PointSymbolizer file="' + cdir2 + '/cache/.*.svg"');
647 |
648 | var pending = 2;
649 | var err = [];
650 | var finish = function (e) {
651 | if (e) {
652 | err.push(e.toString());
653 | }
654 | if (!--pending) {
655 | if (err.length) {
656 | err = new Error(err.join('\n'));
657 | } else {
658 | err = null;
659 | }
660 | done(err);
661 | }
662 | };
663 |
664 | var b1 = store1.mml_builder({ dbname: 'd', sql: 't1', style: style1 });
665 | b1.toXML(function (e, data) {
666 | if (e) { finish(e); return; }
667 | try {
668 | assert.ok(re1.test(data), 'toXML: ' + style + ': expected ' + re1 + ' got:\n' + data);
669 | } catch (e) {
670 | err.push(e);
671 | }
672 | finish();
673 | });
674 |
675 | var b2 = store2.mml_builder({ dbname: 'd', sql: 't2', style: style2 });
676 | b2.toXML(function (e, data) {
677 | if (e) { finish(e); return; }
678 | try {
679 | assert.ok(re2.test(data), 'toXML: ' + style + ': expected ' + re2 + ' got:\n' + data);
680 | } catch (e) {
681 | err.push(e);
682 | }
683 | finish();
684 | });
685 | });
686 |
687 | it('lost XML in base key triggers re-creation', function (done) {
688 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
689 | var mmlBuilder0 = mmlStore.mml_builder({ dbname: 'db', sql: 'SELECT * FROM my_face', style: DEFAULT_POINT_STYLE });
690 | var mmlBuilder = mmlStore.mml_builder({ dbname: 'db', sql: 'SELECT * FROM my_face', style: DEFAULT_POINT_STYLE });
691 | var xml0;
692 | step(
693 | function getXML0 () {
694 | mmlBuilder0.toXML(this);
695 | },
696 | function dropXML0 (err, data) {
697 | if (err) { done(err); return; }
698 | xml0 = data;
699 | return null;
700 | },
701 | function getXML1 (err) {
702 | if (err) { done(err); return; }
703 | mmlBuilder.toXML(this);
704 | },
705 | function checkXML (err, data) {
706 | if (err) { done(err); return; }
707 | assert.equal(data, xml0);
708 | done();
709 | }
710 | );
711 | });
712 |
713 | if (semver.satisfies(new carto.Renderer().options.mapnik_version, '<=2.3.0')) {
714 | // See https://github.com/Vizzuality/grainstore/issues/62
715 | it('throws useful error message on invalid text-name', function (done) {
716 | var style = "#t { text-name: invalid; text-face-name:'Dejagnu'; }";
717 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers, mapnik_version: '2.1.0' });
718 | mmlStore.mml_builder({ dbname: 'd', sql: 't', style: style }).toXML(function (err, xml) {
719 | assert.ok(err);
720 | var re = /Invalid value for text-name/;
721 | assert.ok(err.message.match(re), 'No match for ' + re + ' in "' + err.message + '"');
722 | done();
723 | });
724 | });
725 | }
726 |
727 | it('use exponential in filters', function (done) {
728 | var style = '#t[a=1.2e-3] { polygon-fill: #000000; }';
729 | style += '#t[b=1.2e+3] { polygon-fill: #000000; }';
730 | style += '#t[c=2.3e4] { polygon-fill: #000000; }';
731 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers, mapnik_version: '2.1.0' });
732 | var mmlBuilder = mmlStore.mml_builder({ dbname: 'd2', sql: 't', style: style, styleVersion: '2.1.0' });
733 | step(
734 | function getXML () {
735 | mmlBuilder.toXML(this);
736 | },
737 | function checkXML (err, data) {
738 | if (err) {
739 | throw err;
740 | }
741 | xml2js.parseString(data, (err, xmlDoc) => {
742 | if (err) { done(err); return; }
743 |
744 | const node = xpath.find(xmlDoc, '//Filter');
745 | assert.equal(node.length, 3);
746 |
747 | for (var i = 0; i < node.length; i++) {
748 | var txt = node[i];
749 | if (txt.match(/\[a\] =/)) {
750 | assert.equal(txt, '([a] = 0.0012)');
751 | } else if (txt.match(/\[b\] =/)) {
752 | assert.equal(txt, '([b] = 1200)');
753 | } else if (txt.match(/\[c\] =/)) {
754 | assert.equal(txt, '([c] = 23000)');
755 | } else {
756 | assert.fail('No match for ' + txt);
757 | }
758 | }
759 | });
760 | return null;
761 | },
762 | function finish (err) {
763 | return done(err);
764 | }
765 | );
766 | });
767 |
768 | it('can construct mmlBuilder', function (done) {
769 | var style = '#t {bogus}';
770 | // NOTE: we need mapnik_version to be != 2.0.0
771 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers, mapnik_version: '2.1.0' });
772 | mmlStore.mml_builder({ dbname: 'd', sql: 't', style: style }).toXML(
773 | function checkInitGetXML (err) {
774 | assert.ok(err.message.match(/bogus/), err.message);
775 | done();
776 | }
777 | );
778 | });
779 |
780 | // See https://github.com/CartoDB/grainstore/issues/72
781 | it('invalid fonts are complained about',
782 | function (done) {
783 | var mmlStore = new grainstore.MMLStore({
784 | use_workers: useWorkers,
785 | mapnik_version: '2.1.0',
786 | carto_env: {
787 | validation_data: {
788 | fonts: ['Dejagnu', 'good']
789 | }
790 | }
791 | });
792 | step(
793 | function checkGoodFont () {
794 | mmlStore.mml_builder({ dbname: 'd', sql: 't', style: '#t{text-name:[a]; text-face-name:"good";}' }).toXML(this);
795 | },
796 | function setGoodFont (err) {
797 | if (err) {
798 | throw err;
799 | }
800 | mmlStore.mml_builder(
801 | { dbname: 'd', sql: 't', style: "#t { text-name:[a]; text-face-name:'bogus_font'; }" }
802 | ).toXML(this);
803 | },
804 | function setBogusFont (err) {
805 | assert.ok(err);
806 | assert.ok(err, 'no error raised when using bogus font');
807 | assert.ok(err.message.match(/Invalid.*text-face-name.*bogus_font/), err);
808 | done();
809 | }
810 | );
811 | });
812 |
813 | it('should can set format after building the MML', function (done) {
814 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
815 | var mml = mmlStore.mml_builder({
816 | dbname: 'my_databaasez',
817 | sql: SAMPLE_SQL,
818 | style: DEFAULT_POINT_STYLE
819 | });
820 |
821 | mml.set('grainstore_map', { format: 'png32' });
822 |
823 | mml.toXML(function (err, data) {
824 | if (err) { return done(err); }
825 | xml2js.parseString(data, (err, xmlDoc) => {
826 | if (err) { done(err); return; }
827 |
828 | const format = xpath.find(xmlDoc, "//Parameter[@name='format']")[0];
829 | assert.equal(format._, 'png32');
830 | done();
831 | });
832 | });
833 | });
834 |
835 | it('when setting a property not allowed should throw error', function () {
836 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
837 | var mml = mmlStore.mml_builder({
838 | dbname: 'my_databaasez',
839 | sql: SAMPLE_SQL,
840 | style: DEFAULT_POINT_STYLE
841 | });
842 |
843 | assert.throws(function () {
844 | mml.set('toXML', { format: 'png32' });
845 | }, Error);
846 | });
847 |
848 | it('can set layer name from ids array', function (done) {
849 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
850 | var mml = mmlStore.mml_builder({
851 | dbname: 'my_databaasez',
852 | ids: ['layer-name-wadus'],
853 | sql: SAMPLE_SQL,
854 | style: DEFAULT_POINT_STYLE
855 | });
856 |
857 | mml.toXML(function (err, data) {
858 | if (err) { return done(err); }
859 | xml2js.parseString(data, (err, xmlDoc) => {
860 | if (err) { done(err); return; }
861 |
862 | const layer = xpath.find(xmlDoc, '//Layer')[0];
863 | assert.equal(layer.$.name, 'layer-name-wadus');
864 | done();
865 | });
866 | });
867 | });
868 |
869 | it('set valid interactivity layer name based on ids array', function (done) {
870 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
871 | var mml = mmlStore.mml_builder({
872 | dbname: 'd2',
873 | ids: ['layer-wadus'],
874 | sql: SAMPLE_SQL,
875 | style: DEFAULT_POINT_STYLE,
876 | interactivity: 'a,b'
877 | });
878 | mml.toXML(function (err, data) {
879 | if (err) { return done(err); }
880 | xml2js.parseString(data, (err, xmlDoc) => {
881 | if (err) { done(err); return; }
882 |
883 | var x = xpath.find(xmlDoc, "//Parameter[@name='interactivity_layer']")[0];
884 | assert.ok(x);
885 | assert.equal(x._, 'layer-wadus');
886 | x = xpath.find(xmlDoc, "//Parameter[@name='interactivity_fields']")[0];
887 | assert.ok(x);
888 | assert.equal(x._, 'a,b');
889 | done();
890 | });
891 | });
892 | });
893 |
894 | it('can generate a valid xml without styles', function (done) {
895 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
896 | var mmlBuilder = mmlStore.mml_builder({
897 | dbname: 'my_database',
898 | sql: SAMPLE_SQL
899 | });
900 |
901 | mmlBuilder.toXML((err, xml) => {
902 | if (err) {
903 | return done(err);
904 | }
905 |
906 | xml2js.parseString(xml, (err, xmlDoc) => {
907 | if (err) { done(err); return; }
908 | var x = xpath.find(xmlDoc, "//Parameter[@name='dbname']")[0];
909 | assert.ok(x);
910 | assert.equal(x._, 'my_database');
911 | x = xpath.find(xmlDoc, "//Parameter[@name='table']")[0];
912 | assert.ok(x);
913 | assert.equal(x._, SAMPLE_SQL);
914 |
915 | done();
916 | });
917 | });
918 | });
919 |
920 | describe('minzoom and maxzoom', function () {
921 | const ZOOM_2_SCALE = {
922 | 0: 1000000000,
923 | 1: 500000000,
924 | 2: 200000000,
925 | 3: 100000000,
926 | 4: 50000000,
927 | 5: 25000000,
928 | 6: 12500000,
929 | 7: 6500000,
930 | 8: 3000000,
931 | 9: 1500000,
932 | 10: 750000,
933 | 11: 400000,
934 | 12: 200000,
935 | 13: 100000,
936 | 14: 50000,
937 | 15: 25000,
938 | 16: 12500,
939 | 17: 5000,
940 | 18: 2500,
941 | 19: 1500,
942 | 20: 750,
943 | 21: 500,
944 | 22: 250,
945 | 23: 100,
946 | 24: 50,
947 | 25: 25,
948 | 26: 12.5
949 | };
950 | const zoomScenarios = [
951 | {
952 | desc: 'sets layer minzoom',
953 | zoom: { minzoom: 6 },
954 | // Zooms properties are reversed, using scale denominator ranges.
955 | expectedScale: { maxzoom: ZOOM_2_SCALE[6] }
956 | },
957 | {
958 | desc: 'sets layer maxzoom',
959 | zoom: { maxzoom: 12 },
960 | // Zooms properties are reversed, using scale denominator ranges.
961 | expectedScale: { minzoom: ZOOM_2_SCALE[13] }
962 | },
963 | {
964 | desc: 'sets layer minzoom and maxzoom',
965 | zoom: { minzoom: 6, maxzoom: 18 },
966 | // Zooms properties are reversed, using scale denominator ranges.
967 | expectedScale: {
968 | maxzoom: ZOOM_2_SCALE[6],
969 | minzoom: ZOOM_2_SCALE[19]
970 | }
971 | }
972 | ];
973 | const ZOOM_PROP_2_KEY = {
974 | minzoom: 'maxzoom',
975 | maxzoom: 'minzoom'
976 | };
977 | zoomScenarios.forEach(scenario => {
978 | it(scenario.desc, function (done) {
979 | const mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
980 | const mml = mmlStore.mml_builder({
981 | dbname: 'd2',
982 | ids: ['layer-wadus'],
983 | zooms: [scenario.zoom],
984 | sql: SAMPLE_SQL,
985 | style: DEFAULT_POINT_STYLE
986 | });
987 |
988 | mml.toXML(function (err, xml) {
989 | if (err) { return done(err); }
990 |
991 | xml2js.parseString(xml, (err, xmlDoc) => {
992 | if (err) { done(err); return; }
993 | const layer = xpath.find(xmlDoc, '//Layer')[0].$;
994 | assert.equal(layer.name, 'layer-wadus');
995 |
996 | Object.keys(scenario.zoom).forEach(function (zoomProp) {
997 | // Zooms properties are reversed, using scale denominator ranges.
998 | const zoomAttrKey = ZOOM_PROP_2_KEY[zoomProp];
999 | const zoom = layer[zoomAttrKey];
1000 | const expectedScale = scenario.expectedScale[zoomAttrKey];
1001 | assert.equal(
1002 | zoom,
1003 | expectedScale,
1004 | `Unexpected scale value for '${zoomProp}': got ${zoom}, expected ${expectedScale}`
1005 | );
1006 | });
1007 | });
1008 |
1009 | return done();
1010 | });
1011 | });
1012 | });
1013 | });
1014 |
1015 | it('support global cache-features', function (done) {
1016 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
1017 | var mmlBuilder = mmlStore.mml_builder({
1018 | dbname: 'd2',
1019 | ids: ['layer-features'],
1020 | 'cache-features': true,
1021 | sql: SAMPLE_SQL,
1022 | style: DEFAULT_POINT_STYLE
1023 | });
1024 |
1025 | mmlBuilder.toXML((err, xml) => {
1026 | if (err) { return done(err); }
1027 |
1028 | xml2js.parseString(xml, (err, xmlDoc) => {
1029 | if (err) { done(err); return; }
1030 | const layer = xpath.find(xmlDoc, '//Layer')[0].$;
1031 | assert.equal(layer.name, 'layer-features');
1032 | assert.equal(layer['cache-features'], 'true');
1033 | });
1034 | });
1035 |
1036 | mmlBuilder = mmlStore.mml_builder({
1037 | dbname: 'd2',
1038 | ids: ['layer-features'],
1039 | 'cache-features': false,
1040 | sql: SAMPLE_SQL,
1041 | style: DEFAULT_POINT_STYLE
1042 | });
1043 |
1044 | mmlBuilder.toXML((err, xml) => {
1045 | if (err) { return done(err); }
1046 |
1047 | xml2js.parseString(xml, (err, xmlDoc) => {
1048 | if (err) { done(err); return; }
1049 | const layer = xpath.find(xmlDoc, '//Layer')[0].$;
1050 | assert.equal(layer.name, 'layer-features');
1051 | assert.equal(layer['cache-features'], 'false');
1052 | done();
1053 | });
1054 | });
1055 | });
1056 |
1057 | it('support per layer cache-features', function (done) {
1058 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
1059 | var mmlBuilder = mmlStore.mml_builder({
1060 | dbname: 'd2',
1061 | ids: ['layer-features', ['other-layer']],
1062 | 'cache-features': [true, false],
1063 | sql: [SAMPLE_SQL, SAMPLE_SQL],
1064 | style: DEFAULT_POINT_STYLE
1065 | });
1066 |
1067 | mmlBuilder.toXML((err, xml) => {
1068 | if (err) { return done(err); }
1069 |
1070 | xml2js.parseString(xml, (err, xmlDoc) => {
1071 | if (err) { done(err); return; }
1072 | let layer = xpath.find(xmlDoc, '//Layer')[0].$;
1073 | assert.equal(layer.name, 'layer-features');
1074 | assert.equal(layer['cache-features'], 'true');
1075 |
1076 | layer = xpath.find(xmlDoc, '//Layer')[1].$;
1077 | assert.equal(layer.name, 'other-layer');
1078 | assert.equal(layer['cache-features'], 'false');
1079 |
1080 | done();
1081 | });
1082 | });
1083 | });
1084 |
1085 | it('support per map markers_symbolizer_caches', function (done) {
1086 | var mmlStore = new grainstore.MMLStore({ use_workers: useWorkers });
1087 |
1088 | var mmlBuilder = mmlStore.mml_builder({
1089 | dbname: 'my_database',
1090 | sql: SAMPLE_SQL,
1091 | style: DEFAULT_POINT_STYLE,
1092 | markers_symbolizer_caches: {
1093 | disabled: true
1094 | }
1095 | });
1096 | mmlBuilder.toXML((err, xml) => {
1097 | if (err) { return done(err); }
1098 | xml2js.parseString(xml, (err, xmlDoc) => {
1099 | if (err) { done(err); return; }
1100 | var xpathCaches = "/Map/Parameters/Parameter[@name='markers_symbolizer_caches_disabled']";
1101 | const markersSymbolizerCachesDisabled = xpath.find(xmlDoc, xpathCaches)[0];
1102 | assert.equal(markersSymbolizerCachesDisabled._, 'true');
1103 | });
1104 | });
1105 |
1106 | mmlBuilder = mmlStore.mml_builder({
1107 | dbname: 'my_database',
1108 | sql: SAMPLE_SQL,
1109 | style: DEFAULT_POINT_STYLE,
1110 | markers_symbolizer_caches: {
1111 | disabled: false
1112 | }
1113 | });
1114 | mmlBuilder.toXML((err, xml) => {
1115 | if (err) { return done(err); }
1116 | xml2js.parseString(xml, (err, xmlDoc) => {
1117 | if (err) { done(err); return; }
1118 | var xpathCaches = "/Map/Parameters/Parameter[@name='markers_symbolizer_caches_disabled']";
1119 | const markersSymbolizerCachesDisabled = xpath.find(xmlDoc, xpathCaches)[0];
1120 | assert.equal(markersSymbolizerCachesDisabled._, 'false');
1121 | });
1122 | });
1123 |
1124 | mmlBuilder = mmlStore.mml_builder({
1125 | dbname: 'my_database',
1126 | sql: SAMPLE_SQL,
1127 | style: DEFAULT_POINT_STYLE
1128 | });
1129 | mmlBuilder.toXML((err, xml) => {
1130 | if (err) { return done(err); }
1131 | xml2js.parseString(xml, (err, xmlDoc) => {
1132 | if (err) { done(err); return; }
1133 | var xpathCaches = "/Map/Parameters/Parameter[@name='markers_symbolizer_caches_disabled']";
1134 | const markersSymbolizerCachesDisabled = xpath.find(xmlDoc, xpathCaches);
1135 | assert.equal(markersSymbolizerCachesDisabled.length, 0);
1136 | done();
1137 | });
1138 | });
1139 | });
1140 | });
1141 | });
1142 |
--------------------------------------------------------------------------------