├── 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 | 4 | 5 | 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 | [![Build Status](http://travis-ci.org/CartoDB/grainstore.png?branch=master)] 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 | --------------------------------------------------------------------------------