[node-kosmtik] 01/05: Imported Upstream version 0.0.13

Ross Gammon ross-guest at moszumanska.debian.org
Fri Nov 11 17:33:58 UTC 2016


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

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

commit b819f542cb6a69ef4ef9f1e8c3be4b926dcca5b3
Author: Ross Gammon <rossgammon at mail.dk>
Date:   Thu Nov 10 20:28:02 2016 +0100

    Imported Upstream version 0.0.13
---
 .eslintrc                                        |  23 +
 .gitignore                                       |   7 +
 README.md                                        | 162 ++++++
 favicon.ico                                      | Bin 0 -> 5430 bytes
 index.js                                         |  10 +
 package.json                                     |  37 ++
 screenshot.png                                   | Bin 0 -> 384478 bytes
 src/Config.js                                    | 207 ++++++++
 src/back/ConfigEmitter.js                        |  19 +
 src/back/GeoUtils.js                             |  26 +
 src/back/Helpers.js                              |  12 +
 src/back/MapPool.js                              |  36 ++
 src/back/MetatileBasedTile.js                    |  83 +++
 src/back/PluginsManager.js                       | 153 ++++++
 src/back/PreviewServer.js                        | 149 ++++++
 src/back/Project.js                              | 120 +++++
 src/back/ProjectServer.js                        | 293 +++++++++++
 src/back/StateBase.js                            |  64 +++
 src/back/Tile.js                                 |  44 ++
 src/back/Utils.js                                |  43 ++
 src/back/VectorBasedTile.js                      |  73 +++
 src/back/XRayTile.js                             |  52 ++
 src/back/loader/Base.js                          |  79 +++
 src/back/loader/MML.js                           |  14 +
 src/back/loader/YAML.js                          |  15 +
 src/back/renderer/Carto.js                       |  24 +
 src/back/xray/layer.xml                          |  24 +
 src/back/xray/map.xml                            |  25 +
 src/front/Autocomplete.js                        | 296 +++++++++++
 src/front/Command.js                             | 127 +++++
 src/front/Core.css                               | 632 +++++++++++++++++++++++
 src/front/Core.js                                | 279 ++++++++++
 src/front/DataInspector.js                       | 161 ++++++
 src/front/FormBuilder.js                         |   8 +
 src/front/Map.js                                 | 182 +++++++
 src/front/MetatilesBounds.js                     |  92 ++++
 src/front/Settings.js                            |  60 +++
 src/front/Sidebar.css                            |  87 ++++
 src/front/Sidebar.js                             | 104 ++++
 src/front/Toolbar.css                            |  87 ++++
 src/front/Toolbar.js                             |  30 ++
 src/front/fonts/DejaVuSans-webfont.eot           | Bin 0 -> 367770 bytes
 src/front/fonts/DejaVuSans-webfont.ttf           | Bin 0 -> 983540 bytes
 src/front/fonts/DejaVuSans-webfont.woff          | Bin 0 -> 450340 bytes
 src/front/fonts/FiraSans-Bold.eot                | Bin 0 -> 80661 bytes
 src/front/fonts/FiraSans-Bold.ttf                | Bin 0 -> 158056 bytes
 src/front/fonts/FiraSans-Bold.woff               | Bin 0 -> 88436 bytes
 src/front/fonts/FiraSans-Light.eot               | Bin 0 -> 76059 bytes
 src/front/fonts/FiraSans-Light.ttf               | Bin 0 -> 158100 bytes
 src/front/fonts/FiraSans-Light.woff              | Bin 0 -> 83532 bytes
 src/front/fonts/FiraSans-Regular.eot             | Bin 0 -> 76088 bytes
 src/front/fonts/FiraSans-Regular.ttf             | Bin 0 -> 158200 bytes
 src/front/fonts/FiraSans-Regular.woff            | Bin 0 -> 83300 bytes
 src/front/header_logo.svg                        |  83 +++
 src/front/logo.svg                               | 285 ++++++++++
 src/front/project.html                           |  16 +
 src/plugins/base-exporters/Base.js               |  10 +
 src/plugins/base-exporters/MML.js                |  14 +
 src/plugins/base-exporters/PNG.js                |  66 +++
 src/plugins/base-exporters/XML.js                |  14 +
 src/plugins/base-exporters/YAML.js               |  15 +
 src/plugins/base-exporters/front/export.js       | 278 ++++++++++
 src/plugins/base-exporters/index.js              |  81 +++
 src/plugins/datasource-loader/index.js           |  63 +++
 src/plugins/hash/index.js                        |  27 +
 src/plugins/local-config/index.js                |  57 ++
 test/config.js                                   |  16 +
 test/config.yml                                  |   0
 test/data/expected/tile.world.0.0.0.png          | Bin 0 -> 18322 bytes
 test/data/expected/tile.world.6.19.28.geojson    |   2 +
 test/data/expected/tile.world.6.19.28.pbf        | Bin 0 -> 231 bytes
 test/data/expected/tile.world.6.19.28.png        | Bin 0 -> 4303 bytes
 test/data/minimalist-project.mml                 |  40 ++
 test/data/minimalist-project.xml                 |  25 +
 test/data/minimalist-project.yml                 |  31 ++
 test/data/tree/afile.txt                         |   0
 test/data/tree/subdir/anotherfile.js             |   0
 test/data/tree/subdir/anothersubdir/yetafile.csv |   0
 test/data/world/project.yml                      |  35 ++
 test/data/world/style.mss                        |   8 +
 test/data/world/world.geojson                    | 179 +++++++
 test/exporter.js                                 |  17 +
 test/geoutils.js                                 |  10 +
 test/loader.js                                   |  27 +
 test/tile.js                                     |  83 +++
 test/utils.js                                    |  18 +
 86 files changed, 5439 insertions(+)

diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..6dea436
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,23 @@
+{
+    "env": {
+        "browser": true,
+        "node": true
+    },
+    "rules": {
+        "quotes": [2, "single"],
+        "no-underscore-dangle": 0,
+        "curly": 0,
+        "consistent-return": 0,
+        "new-cap": 0,
+        "strict": [2, "never"],
+        "indent": [2, 4],
+        "no-shadow": 0,
+        "semi-spacing": 0,
+        "semi": [2, "always"]
+
+    },
+    "globals": {
+        "L": true,
+        "kosmtik": true
+    }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..70921f4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+node_modules/*
+notes.txt
+npm-debug.log
+logo.png
+v8.log
+tmp/
+plugins-src/*
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7f94ddb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,162 @@
+# Kosmtik
+
+[![Join the chat at https://gitter.im/kosmtik/kosmtik](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/kosmtik/kosmtik?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[![Dependency Status](https://david-dm.org/kosmtik/kosmtik.svg)](https://david-dm.org/kosmtik/kosmtik)
+
+Very lite but extendable mapping framework to create Mapnik ready maps with
+OpenStreetMap data (and more).
+
+For now, only Carto based projects are supported (with .mml or .yml config),
+but in the future we hope to plug in MapCSS too.
+
+**Alpha version, installable only from source**
+
+
+## Lite
+
+Only the core needs:
+
+- project loading
+- local configuration management
+- tiles server for live feedback when coding
+- exports to common formats (Mapnik XML, PNG…)
+- hooks everywhere to make easy to extend it with plugins
+
+
+## Screenshot
+
+![screenshot](https://raw.github.com/kosmtik/kosmtik/master/screenshot.png "Screenshot of Kosmtik")
+
+
+## Install
+
+Clone this repository with ``git clone https://github.com/kosmtik/kosmtik.git``,
+go to the downloaded directory with ``cd kosmtik``, and run:
+
+```
+npm install
+```
+
+## Update
+
+Obtain changes from repository (e.g. `git pull`)
+
+    rm -rf node_modules && npm install
+
+To reinstall all plugins:
+
+    node index.js plugins --reinstall
+
+## Usage
+
+To get command line help, run:
+
+```
+node index.js -h
+```
+
+To run a Carto project (or `.yml`, `.yaml`):
+
+```
+node index.js serve <path/to/your/project.mml>
+```
+
+Then open your browser at http://127.0.0.1:6789/.
+
+
+You may also want to install plugins. To see the list of available ones, type:
+
+```
+node index.js plugins --available
+```
+
+And then pick one and install it like this:
+```
+node index.js plugins --install pluginname
+```
+
+For example:
+```
+node index.js plugins --install kosmtik-map-compare [--install kosmtik-overlay…]
+```
+
+
+## Local config
+
+Because you often need to change the project config to match your
+local env, for example to adapt the database connection credentials,
+kosmtik comes with an internal plugin to manage that. You have two
+options: with a json file named `localconfig.json`, or with a js module
+name `localconfig.js`.
+
+Place your localconfig.js or localconfig.json file in the same directory as your 
+carto project (or `.yml`, `.yaml`).
+
+In both cases, the behaviour is the same, you create some rules to target
+the configuration and changes the values. Those rules are started by the
+keyword `where`, and you define which changes to apply using `then`
+keyword. You can also filter the targeted objects by using the `if` clause.
+See the examples below to get it working right now.
+
+
+
+### Example of a json file
+```
+[
+    {
+        "where": "center",
+        "then": [29.9377, -3.4216, 9]
+    },
+    {
+        "where": "Layer",
+        "if": {
+            "Datasource.type": "postgis"
+        },
+        "then": {
+            "Datasource.dbname": "burundi",
+            "Datasource.password": "",
+            "Datasource.user": "ybon",
+            "Datasource.host": ""
+        }
+    },
+    {
+        "where": "Layer",
+        "if": {
+            "id": "hillshade"
+        },
+        "then": {
+            "Datasource.file": "/home/ybon/Code/maps/hdm/DEM/data/hillshade.vrt"
+        }
+    }
+]
+```
+
+### Example of a js module
+```
+exports.LocalConfig = function (localizer, project) {
+    localizer.where('center').then([29.9377, -3.4216, 9]);
+    localizer.where('Layer').if({'Datasource.type': 'postgis'}).then({
+        "Datasource.dbname": "burundi",
+        "Datasource.password": "",
+        "Datasource.user": "ybon",
+        "Datasource.host": ""
+    });
+    // You can also do it in pure JS
+    project.mml.bounds = [1, 2, 3, 4];
+};
+
+```
+
+## Known plugins
+
+- [kosmtik-overpass-layer](https://github.com/kosmtik/kosmtik-overpass-layer): add Overpass Layer in your carto project
+- [kosmtik-fetch-remote](https://github.com/kosmtik/kosmtik-fetch-remote): automagically fetch remote files in your layers
+- [kosmtik-place-search](https://github.com/kosmtik/kosmtik-place-search): search places control
+- [kosmtik-overlay](https://github.com/kosmtik/kosmtik-overlay): add an overlay above the map
+- [kosmtik-open-in-josm](https://github.com/kosmtik/kosmtik-open-in-josm): open JOSM with current view
+- [kosmtik-map-compare](https://github.com/kosmtik/kosmtik-map-compare): display a map side-by-side with your work
+- [kosmtik-osm-data-overlay](https://github.com/kosmtik/kosmtik-osm-data-overlay): display OSM data on top of your map
+- [kosmtik-tiles-export](https://github.com/kosmtik/kosmtik-tiles-export): export a tiles tree from your project
+- [kosmtik-mbtiles-export](https://github.com/kosmtik/kosmtik-mbtiles-export): export your project in MBTiles
+
+Run `node index.js plugins --available` to get an up to date list.
diff --git a/favicon.ico b/favicon.ico
new file mode 100644
index 0000000..251fa53
Binary files /dev/null and b/favicon.ico differ
diff --git a/index.js b/index.js
new file mode 100755
index 0000000..cdccdf4
--- /dev/null
+++ b/index.js
@@ -0,0 +1,10 @@
+#!/usr/bin/env node
+
+var Config = require('./src/Config.js').Config,
+    Server = require('./src/back/PreviewServer.js').PreviewServer;
+
+process.title = 'kosmtik';
+
+var config = new Config(__dirname, process.env.KOSMTIK_CONFIGPATH);
+var server = new Server(config, __dirname);
+config.parseOptions();
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..34b24ae
--- /dev/null
+++ b/package.json
@@ -0,0 +1,37 @@
+{
+  "name": "kosmtik",
+  "version": "0.0.13",
+  "description": "Make maps with OpenStreetMap and Mapnik",
+  "main": "index.js",
+  "scripts": {
+    "test": "node node_modules/.bin/mocha"
+  },
+  "keywords": [
+    "openstreetmap",
+    "map",
+    "design"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/kosmtik/kosmtik.git"
+  },
+  "author": "Yohan Boniface",
+  "license": "WTFPL",
+  "dependencies": {
+    "carto": "^0.15.2",
+    "generic-pool": "^2.2.0",
+    "js-yaml": "^3.4.2",
+    "json-localizer": "0.0.3",
+    "leaflet": "^1.0.0-beta.2",
+    "leaflet-formbuilder": "^0.2.0",
+    "leaflet-hash": "^0.2.1",
+    "mapnik": "^3.4.7",
+    "nomnom": "^1.8.1",
+    "npm": "^3.3.5",
+    "request": "^2.64.0",
+    "semver": "^5.0.3"
+  },
+  "devDependencies": {
+    "mocha": "^2.2.5"
+  }
+}
diff --git a/screenshot.png b/screenshot.png
new file mode 100644
index 0000000..ce49ba4
Binary files /dev/null and b/screenshot.png differ
diff --git a/src/Config.js b/src/Config.js
new file mode 100644
index 0000000..30afc37
--- /dev/null
+++ b/src/Config.js
@@ -0,0 +1,207 @@
+var util = require('util'),
+    path = require('path'),
+    fs = require('fs'),
+    semver = require('semver'),
+    yaml = require('js-yaml'),
+    StateBase = require('./back/StateBase.js').StateBase,
+    Helpers = require('./back/Helpers.js').Helpers,
+    mapnik = require('mapnik'),
+    PluginsManager = require('./back/PluginsManager.js').PluginsManager;
+
+GLOBAL.kosmtik = {};
+kosmtik.src = __dirname;
+
+var Config = function (root, configpath) {
+    StateBase.call(this);
+    this.configpath = configpath;
+    this.root = root;
+    this.helpers = new Helpers(this);
+    this.initOptions();
+    this.initExporters();
+    this.initLoaders();
+    this.initStatics();
+    if (!this.configpath) this.ensureDefaultUserConfigPath();
+    this.loadUserConfig();
+    this.pluginsManager = new PluginsManager(this);  // Do we need back ref?
+    this.emit('loaded');
+    this.on('server:init', this.attachRoutes.bind(this));
+    this.parsed_opts = {};  // Default. TODO better option management.
+};
+
+util.inherits(Config, StateBase);
+
+Config.prototype.getUserConfigDir = function () {
+    var home = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
+    return path.join(home, '.config');
+};
+
+Config.prototype.getUserConfigPath = function () {
+    return this.configpath || path.join(this.getUserConfigDir(), 'kosmtik.yml');
+};
+
+Config.prototype.loadUserConfig = function () {
+    var configpath = this.getUserConfigPath(),
+        config = {};
+    try {
+        config = yaml.safeLoad(fs.readFileSync(configpath, 'utf-8'));
+        this.log('Loading config from', configpath);
+    } catch (err) {
+        this.log('No usable config file found in', configpath);
+    }
+    this.userConfig = config;
+};
+
+Config.prototype.saveUserConfig = function () {
+    var configpath = this.getUserConfigPath(),
+        self = this;
+    fs.writeFile(configpath, yaml.safeDump(this.userConfig), function (err) {
+        self.log('Saved env conf to', configpath);
+    });
+};
+
+Config.prototype.getFromUserConfig = function (key, fallback) {
+    return typeof this.userConfig[key] !== 'undefined' ? this.userConfig[key] : fallback;
+};
+
+Config.prototype.ensureDefaultUserConfigPath = function () {
+    try {
+        fs.mkdirSync(this.getUserConfigDir());
+    } catch (err) {
+        if (err.code !== 'EEXIST') throw err;
+    }
+};
+
+Config.prototype.initExporters = function () {
+    this.exporters = {};
+};
+
+Config.prototype.registerExporter = function (format, path) {
+    this.exporters[format] = path;
+};
+
+Config.prototype.initLoaders = function () {
+    this.loaders = {};
+    this.registerLoader('.mml', './back/loader/MML.js');
+    this.registerLoader('.yml', './back/loader/YAML.js');
+    this.registerLoader('.yaml', './back/loader/YAML.js');
+};
+
+Config.prototype.registerLoader = function (ext, nameOrPath) {
+    this.loaders[ext] = nameOrPath;
+};
+
+Config.prototype.getLoader = function (ext) {
+    if (!this.loaders[ext]) throw 'Unkown project config type: ' + ext;
+    return require(this.loaders[ext]).Loader;
+};
+
+Config.prototype.initOptions = function () {
+    this.opts = require('nomnom');
+    this.commands = {};
+    this.commands.serve = this.opts.command('serve').help('Run the server');
+    this.commands.serve.option('path', {
+        position: 1,
+        help: 'Optional project path to load at start.'
+    });
+    this.commands.serve.option('port', {
+        default: 6789,
+        help: 'Port to listen on.'
+    });
+    this.commands.serve.option('host', {
+        default: '127.0.0.1',
+        help: 'Host to listen on.'
+    });
+    this.opts.option('mapnik_version', {
+        full: 'mapnik-version',
+        default: this.defaultMapnikVersion(),
+        help: 'Optional mapnik reference version to be passed to Carto'
+    });
+    this.opts.option('proxy', {
+        help: 'Optional proxy to use when doing http requests'
+    });
+    this.opts.option('keepcache', {
+        full: 'keep-cache',
+        flag: true,
+        help: 'Do not flush cached metatiles on project load'
+    });
+};
+
+Config.prototype.parseOptions = function () {
+    // Make sure to include all formats, even the ones
+    // added by plugins.
+    this.emit('parseopts');
+    this.parsed_opts = this.opts.parse();
+    this.emit('command:' + this.parsed_opts[0]);
+};
+
+Config.prototype.initStatics = function () {
+    this._js = [
+        '/node_modules/leaflet/dist/leaflet-src.js',
+        '/node_modules/leaflet-formbuilder/Leaflet.FormBuilder.js',
+        '/src/front/Core.js',
+        '/config/',
+        './config/',
+        '/src/front/Autocomplete.js',
+        '/src/front/DataInspector.js',
+        '/src/front/MetatilesBounds.js',
+        '/src/front/Sidebar.js',
+        '/src/front/Toolbar.js',
+        '/src/front/FormBuilder.js',
+        '/src/front/Settings.js',
+        '/src/front/Command.js',
+        '/src/front/Map.js'
+    ];
+    this._css = [
+        '/node_modules/leaflet/dist/leaflet.css',
+        '/src/front/Sidebar.css',
+        '/src/front/Toolbar.css',
+        '/src/front/Core.css'
+    ];
+};
+
+Config.prototype.addJS = function (path) {
+    this._js.push(path);
+};
+
+Config.prototype.addCSS = function (path) {
+    this._css.push(path);
+};
+
+Config.prototype.toFront = function () {
+    var options = {
+        exportFormats: Object.keys(this.exporters),
+        autoReload: this.getFromUserConfig('autoReload', true),
+        backendPolling: true,
+        showCrosshairs: true,
+        dataInspectorLayers: {
+            '__all__': true
+        }
+    };
+    this.emit('tofront', {options: options});
+    return options;
+};
+
+Config.prototype.attachRoutes = function (e) {
+    e.server.addRoute('/config/', this.serveForFront.bind(this));
+};
+
+Config.prototype.serveForFront = function (req, res) {
+    res.writeHead(200, {
+        'Content-Type': 'application/javascript'
+    });
+    var tpl = 'L.K.Config = %;';
+    res.write(tpl.replace('%', JSON.stringify(this.toFront())));
+    res.end();
+};
+
+Config.prototype.log = function () {
+    console.warn.apply(console, Array.prototype.concat.apply(['[Core]'], arguments));
+};
+
+Config.prototype.defaultMapnikVersion = function () {
+    var version = semver(mapnik.versions.mapnik);
+    version.patch = 0;
+    return version.format();
+};
+
+exports.Config = Config;
diff --git a/src/back/ConfigEmitter.js b/src/back/ConfigEmitter.js
new file mode 100644
index 0000000..a1081b0
--- /dev/null
+++ b/src/back/ConfigEmitter.js
@@ -0,0 +1,19 @@
+var util = require('util'),
+    StateBase = require('./StateBase.js').StateBase;
+
+var ConfigEmitter = function (config) {
+    StateBase.call(this);
+    this.config = config;
+};
+
+util.inherits(ConfigEmitter, StateBase);
+
+ConfigEmitter.prototype.emitAndForward = function (type, e) {
+    this.emit(type, e);
+    e = e || {};
+    e[this.CLASSNAME] = this;
+    type = this.CLASSNAME + ':' + type;
+    StateBase.prototype.emit.call(this.config, type, e);
+};
+
+exports.ConfigEmitter = ConfigEmitter;
diff --git a/src/back/GeoUtils.js b/src/back/GeoUtils.js
new file mode 100644
index 0000000..ecca4c7
--- /dev/null
+++ b/src/back/GeoUtils.js
@@ -0,0 +1,26 @@
+var sinh = require('./Utils.js').sinh;
+
+module.exports = {
+
+    zoomXYToLatLng: function (z, x, y) {
+        var n = Math.pow(2.0, z),
+            lonDeg = x / n * 360.0 - 180.0,
+            latRad = Math.atan(sinh(Math.PI * (1 - 2 * y / n))),
+            latDeg = latRad * 180.0 / Math.PI;
+        return [lonDeg, latDeg];
+    },
+
+    zoomLatLngToXY: function (z, lat, lng) {
+        var xy = module.exports.zoomLatLngToFloatXY(z, lat, lng);
+        return [Math.floor(xy[0]), Math.floor(xy[1])];
+    },
+
+    zoomLatLngToFloatXY: function (z, lat, lng) {
+        var n = Math.pow(2.0, z),
+            latRad = lat / 180.0 * Math.PI,
+            y = (1.0 - Math.log(Math.tan(latRad) + (1 / Math.cos(latRad))) / Math.PI) / 2.0 * n,
+            x = ((lng + 180.0) / 360.0) * n;
+        return [x, y];
+    }
+
+};
diff --git a/src/back/Helpers.js b/src/back/Helpers.js
new file mode 100644
index 0000000..1cc0b1c
--- /dev/null
+++ b/src/back/Helpers.js
@@ -0,0 +1,12 @@
+var request = require('request');
+
+var Helpers = function (config) {
+    this.config = config;
+};
+
+Helpers.prototype.request = function (options, callback) {
+    if(this.config.parsed_opts.proxy) options.proxy = this.config.parsed_opts.proxy;
+    return request(options, callback);
+};
+
+exports.Helpers = Helpers;
diff --git a/src/back/MapPool.js b/src/back/MapPool.js
new file mode 100644
index 0000000..d9f5b71
--- /dev/null
+++ b/src/back/MapPool.js
@@ -0,0 +1,36 @@
+// Ship our own version of mapnik-pool until
+// this get merged and published
+// https://github.com/mapbox/mapnik-pool/pull/2
+var Pool = require('generic-pool').Pool,
+    os = require('os');
+
+var N_CPUS = os.cpus().length,
+    defaultOptions = { size: 256 },
+    defaultMapOptions = { };
+
+module.exports = function(mapnik) {
+    return {
+        fromString: function(xml, options, mapOptions) {
+            mapOptions = mapOptions || {};
+            return Pool({
+                create: create,
+                destroy: destroy,
+                max: N_CPUS
+            });
+            function create(callback) {
+                var map = new mapnik.Map(options.size, options.size);
+                map.fromString(xml, mapOptions, loaded);
+                function loaded(err) {
+                    if (err) return callback(err);
+                    if (options.bufferSize) {
+                        map.bufferSize = options.bufferSize;
+                    }
+                    return callback(err, map);
+                }
+            }
+            function destroy(map) {
+                delete map;
+            }
+        }
+    };
+};
diff --git a/src/back/MetatileBasedTile.js b/src/back/MetatileBasedTile.js
new file mode 100644
index 0000000..341564d
--- /dev/null
+++ b/src/back/MetatileBasedTile.js
@@ -0,0 +1,83 @@
+var fs = require('fs'),
+    mapnik = require('mapnik'),
+    Tile = require('./Tile.js').Tile,
+    path = require('path');
+
+var MetatileBasedTile = function (z, x, y, options) {
+    this.z = z;
+    this.x = x;
+    this.y = y;
+    this.metatile = options.metatile || 1;
+    this.metaX = Math.floor(x / this.metatile);
+    this.metaY = Math.floor(y / this.metatile);
+    this.options = options;
+};
+
+MetatileBasedTile.prototype.render = function (project, map, cb) {
+    var self = this,
+        metaPath = path.join(project.cachePath, this.z + '.' + this.metaX + '.' + this.metaY + '.meta'),
+        lockPath = path.join(project.cachePath, this.z + '.' + this.metaX + '.' + this.metaY + '.lock');
+
+    fs.readFile(metaPath, function (err, data) {
+        if (err) {
+            if (err.code !== 'ENOENT') return cb(err);
+            fs.writeFile(lockPath, '', {flag: 'wx'}, function (err) {
+                if (err && err.code === 'EEXIST') {
+                    try {
+                        var watcher = fs.watch(lockPath);
+                        watcher.on('change', function (event) {  // Someone else is building the metatile, keep calm and wait.
+                            if (event === 'rename') { // lock has been deleted
+                                watcher.close();
+                                self.render(project, map, cb);  // Try again
+                            }
+                            // else just wait again
+                        });
+                    } catch (err) {
+                        if (err && err.code !== 'ENOENT') return cb(err);
+                    }
+                } else if (err && err.code !== 'EEXIST') {
+                    return cb(err);
+                } else  {
+                    if (err) return cb(err);
+                    self.renderMetatile(metaPath, project, map, function (err, buffer) {
+                        fs.unlink(lockPath, function (err2) {
+                            if (err) return cb(err);
+                            if (err2 && err2.code !== 'ENOENT') return cb(err2);
+                            self.extractFromBytes(buffer, cb);
+                        });
+                    });
+                }
+            });
+        } else {
+            self.extractFromBytes(data, cb);
+        }
+    });
+
+};
+
+MetatileBasedTile.prototype.extractFromBytes = function (buffer, cb) {
+    var self = this;
+    mapnik.Image.fromBytes(buffer, function (err, im) {
+        if (err) return cb(err);
+        var view = im.view(256 * (self.x % self.metatile), 256 * (self.y % self.metatile), 256, 256);
+        cb(null, view);
+    });
+
+};
+
+MetatileBasedTile.prototype.renderMetatile = function (metaPath, project, map, cb) {
+    var self = this;
+    var tile = new Tile(self.z, self.metaX, self.metaY, {size: this.options.metatile * 256, scale: this.options.metatile});
+    tile.render(project, map, function (err, im) {
+        if (err) return cb(err);
+        im.encode('png', function (err, buffer) {
+            if (err) return cb(err);
+            fs.writeFile(metaPath, buffer, {flag: 'wx'}, function (err) {
+                if (err && err.code !== 'EEXIST') return cb(err);
+                cb(null, buffer);
+            });
+        });
+    });
+};
+
+exports.Tile = MetatileBasedTile;
diff --git a/src/back/PluginsManager.js b/src/back/PluginsManager.js
new file mode 100644
index 0000000..0f189c9
--- /dev/null
+++ b/src/back/PluginsManager.js
@@ -0,0 +1,153 @@
+var npm = require('npm'),
+    fs = require('fs'),
+    path = require('path'),
+    semver = require('semver');
+
+var PluginsManager = function (config) {
+    this.config = config;
+    this.config.commands.plugins = this.config.opts.command('plugins');
+    this.config.commands.plugins.option('installed', {
+        flag: true,
+        help: 'Show installed plugins'
+    }).help('Manage plugins');
+    this.config.commands.plugins.option('available', {
+        flag: true,
+        help: 'Show available plugins in registry'
+    });
+    this.config.commands.plugins.option('install', {
+        metavar: 'NAME',
+        help: 'Install a plugin',
+        list: true
+    });
+    this.config.commands.plugins.option('reinstall', {
+        flag: true,
+        help: 'Reinstall every installed plugin'
+    });
+    this.config.on('command:plugins', this.handleCommand.bind(this));
+    this._registered = [
+        '../plugins/base-exporters/index.js',
+        '../plugins/hash/index.js',
+        '../plugins/local-config/index.js',
+        '../plugins/datasource-loader/index.js'
+    ].concat(this.config.userConfig.plugins || []);
+    for (var i = 0; i < this._registered.length; i++) {
+        this.load(this._registered[i]);
+    }
+};
+
+PluginsManager.prototype.load = function (name_or_path) {
+    var Plugin;
+    try {
+        Plugin = require(name_or_path).Plugin;
+    } catch (err) {
+        this.config.log('Unable to load plugin', name_or_path, err.code);
+        return;
+    }
+    this.config.log('Loading plugin from', name_or_path);
+    new Plugin(this.config);
+};
+
+PluginsManager.prototype.each = function (method, context) {
+    for (var i = 0; i < this._registered.length; i++) {
+        method.call(context || this, this._registered[i]);
+    }
+};
+
+PluginsManager.prototype.isInstalled = function (name) {
+    return this._registered.indexOf(name) !== -1;
+};
+
+PluginsManager.prototype.isLocal = function (name) {
+    return name.indexOf('/') !== -1;
+};
+
+PluginsManager.prototype.loadPackage = function () {
+    return JSON.parse(fs.readFileSync(path.join(this.config.root, 'package.json')));
+};
+
+PluginsManager.prototype.available = function (callback) {
+    npm.load(this.loadPackage(), function () {
+        npm.commands.search(['kosmtik'], true, function (err, results) {
+            if (err) return callback(err);
+            var plugin, plugins = [];
+            for (var name in results) {
+                plugin = results[name];
+                if (plugin.keywords.indexOf('kosmtik') === -1) continue;
+                plugins.push(plugin);
+            }
+            callback(null, plugins);
+        });
+    });
+
+};
+
+PluginsManager.prototype.install = function (names) {
+    var self = this,
+        pkg = this.loadPackage(),
+        i = 0;
+    var loopInstall = function () {
+        var name = names[i++];
+        if (!name) return;
+        self.config.log('Starting installation of ' + name);
+        npm.load(pkg, function () {
+            npm.commands.view([name], true, function (err, data) {
+                if (err) throw err.message;
+                var version = Object.keys(data)[0];
+                if (!version) return self.config.log('Not found', name, 'ABORTING');
+                var plugin = data[version];
+                if (!plugin.kosmtik || !semver.satisfies(pkg.version, plugin.kosmtik)) {
+                    return self.config.log('Unable to install', name, 'version', plugin.kosmtik, 'does not satisfy local kosmtik install', pkg.version, 'ABORTING');
+                }
+                npm.commands.install([name], function (err) {
+                    if (err) return self.config.log('Error when installing package', name, err);
+                    self.config.log('Successfully installed package', name);
+                    self.attach(plugin.name);
+                    self.config.saveUserConfig();
+                    loopInstall();
+                });
+            });
+        });
+    };
+    loopInstall();
+};
+
+PluginsManager.prototype.reinstall = function () {
+    var names = [];
+    this.each(function (name) {
+        if (!this.isLocal(name)) names.push(name);
+    });
+    this.install(names);
+};
+
+PluginsManager.prototype.attach = function (name) {
+    // Attach plugin to user config
+    this.config.userConfig.plugins = this.config.userConfig.plugins || [];
+    if (this.config.userConfig.plugins.indexOf(name) === -1) this.config.userConfig.plugins.push(name);
+    this.config.log('Attached plugin:', name);
+};
+
+PluginsManager.prototype.handleCommand = function () {
+    var self = this, installed;
+    if (this.config.parsed_opts.installed) {
+        console.log('Installed plugins');
+        for (var i = 0; i < this._registered.length; i++) {
+            console.log(this._registered[i]);
+        }
+    } else if (this.config.parsed_opts.available) {
+        console.log('Loading available plugins…');
+        this.available(function (err, plugins) {
+            if (err) throw err.message;
+            for (var i = 0, plugin; i < plugins.length; i++) {
+                plugin = plugins[i];
+                installed = self.isInstalled(plugin.name) ? '✓ ' : '. ';
+                console.log(installed, plugin.name, '(' + plugin.description + ')');
+            }
+        });
+    } else if (this.config.parsed_opts.install) {
+        this.install(this.config.parsed_opts.install);
+    } else if (this.config.parsed_opts.reinstall) {
+        this.reinstall();
+    }
+};
+
+exports.PluginsManager = PluginsManager;
diff --git a/src/back/PreviewServer.js b/src/back/PreviewServer.js
new file mode 100644
index 0000000..947bf77
--- /dev/null
+++ b/src/back/PreviewServer.js
@@ -0,0 +1,149 @@
+var http = require('http'),
+    url = require('url'),
+    fs = require('fs'),
+    util = require('util'),
+    path = require('path'),
+    ConfigEmitter = require('./ConfigEmitter.js').ConfigEmitter,
+    Project = require('./Project.js').Project,
+    ProjectServer = require('./ProjectServer.js').ProjectServer,
+    MIMES = {
+        '.html' : 'text/html',
+        '.css' : 'text/css',
+        '.js' : 'application/javascript',
+        '.png' : 'image/png',
+        '.gif' : 'image/gif',
+        '.jpg' : 'image/jpeg',
+        '.woff' : 'application/octet-stream',
+        '.ttf' : 'application/octet-stream',
+        '.svg' : 'image/svg+xml',
+        '.ico' : 'image/x-icon'
+    };
+
+var PreviewServer = function (config, root, options) {
+    this.CLASSNAME = 'server';
+    ConfigEmitter.call(this, config);
+    this.config.server = this;
+    this.initRoutes();
+    options = options || {};
+    this.projects = {};
+    this.server = http.createServer();
+    this.server.on('request', this.serve.bind(this));
+    this.server.timeout = 0;
+    this.root = root;
+    this.emitAndForward('init');
+    this.config.on('command:serve', this.listen.bind(this));
+};
+
+util.inherits(PreviewServer, ConfigEmitter);
+
+PreviewServer.prototype.listen = function () {
+    if (this.config.parsed_opts.path) {
+        var project = new Project(this.config, this.config.parsed_opts.path);
+        this.registerProject(project);
+        this.setDefaultProject(project);
+    }
+    this.server.listen(this.config.parsed_opts.port, this.config.parsed_opts.host);
+    this.config.log('PreviewServer started, you can browse http://' + this.config.parsed_opts.host + ':' + this.config.parsed_opts.port);
+    this.emitAndForward('listen');
+};
+
+PreviewServer.prototype.registerProject = function (project) {
+    this.projects[project.id] = new ProjectServer(project, this);  // TODO avoid cross ref
+};
+
+PreviewServer.prototype.setDefaultProject = function (project) {
+    this.defaultProject = project;
+};
+
+PreviewServer.prototype.serve = function (req, res) {
+    res.on('finish', function () {
+        // 204 are empty responses from poller, do not pollute
+        if (this.statusCode !== 204) console.warn('[httpserver]', req.url, this.statusCode);
+    });
+    var uri = url.parse(req.url, true),
+        urlpath = uri.pathname,
+        els = urlpath.split('/');
+    if (urlpath === '/') this.serveHome(uri, req, res);
+    else if (this.hasRoute(urlpath)) this._routes[urlpath].call(this, req, res);
+    else if (this.projects[els[1]]) this.forwardToProject(uri, els[1], res);
+    else this.serveFile(path.join(this.root, urlpath), res);
+};
+
+PreviewServer.prototype.forwardToProject = function (uri, id, res) {
+    uri.pathname = uri.pathname.replace('/' + id, '');
+    this.projects[id].serve(uri, res);
+};
+
+PreviewServer.prototype.serveHome = function (uri, req, res) {
+    // Go to project for now
+    if (this.defaultProject) return this.redirect(this.defaultProject.id, res);
+    return this.serveFile(path.join(kosmtik.src, 'front/index.html'), res);
+};
+
+PreviewServer.prototype.redirect = function (newuri, res) {
+    res.writeHead(302, {'Location': newuri, 'Cache-Control': 'private, no-cache, must-revalidate'});
+    res.end();
+};
+
+PreviewServer.prototype.serveFile = function (filepath, res) {
+    var self = this,
+        ext = path.extname(filepath);
+    if (!MIMES[ext]) return this.notFound(filepath, res);
+    fs.exists(filepath, function(exists) {
+        if (exists) {
+            fs.readFile(filepath, function(err, contents) {
+                if(!err) {
+                    res.writeHead(200, {
+                        'Content-Type': MIMES[ext],
+                        'Content-Length' : contents.length
+                    });
+                    res.end(contents);
+                }
+            });
+        } else {
+            self.notFound(filepath, res);
+        }
+    });
+};
+
+PreviewServer.prototype.notFound = function (filepath, res) {
+    res.writeHead(404);
+    res.end('Not Found: ' + filepath);
+};
+
+PreviewServer.prototype.initRoutes = function () {
+    this._routes = {};
+    this._project_routes = {};
+};
+
+PreviewServer.prototype.addRoute = function (path, callback) {
+    this._routes[path] = callback;
+};
+
+PreviewServer.prototype.hasRoute = function (path) {
+    return !!this._routes[path];
+};
+
+PreviewServer.prototype.addProjectRoute = function (path, callback) {
+    this._project_routes[path] = callback;
+};
+
+PreviewServer.prototype.hasProjectRoute = function (path) {
+    return !!this._project_routes[path];
+};
+
+PreviewServer.prototype.serveProjectRoute = function (path, uri, res, project) {
+    return this._project_routes[path].call(this, uri, res, project);
+};
+
+PreviewServer.prototype.pushToFront = function (res, anonymous) {
+    // Ugly but GOOD
+    if (anonymous.name) throw 'Cannot use bridge helper with named function:' + anonymous.name;
+    res.writeHead(200, {
+        'Content-Type': 'application/javascript',
+    });
+    res.write(anonymous.toString().substring(13, anonymous.toString().length - 1));
+    res.end();
+};
+
+exports.PreviewServer = PreviewServer;
diff --git a/src/back/Project.js b/src/back/Project.js
new file mode 100644
index 0000000..01cbd72
--- /dev/null
+++ b/src/back/Project.js
@@ -0,0 +1,120 @@
+var util = require('util'),
+    path = require('path'),
+    fs = require('fs'),
+    ConfigEmitter = require('./ConfigEmitter.js').ConfigEmitter,
+    Utils = require('./Utils.js');
+
+var Project = function (config, filepath, options) {
+    options = options || {};
+    this.CLASSNAME = 'project';
+    ConfigEmitter.call(this, config);
+    this.filepath = filepath;
+    this.id = options.id || path.basename(path.dirname(fs.realpathSync(this.filepath)));
+    this.root = path.dirname(path.resolve(this.filepath));
+    this.dataDir = path.join(this.root, 'data');
+    try {
+        fs.mkdirSync(this.dataDir);
+    } catch (err) {}
+    this.mapnik = require('mapnik');
+    this.mapnikPool = require('./MapPool.js')(this.mapnik);
+    this.mapnik.register_default_fonts();
+    this.mapnik.register_system_fonts();
+    this.mapnik.register_default_input_plugins();
+    this.mapnik.register_fonts(path.join(path.dirname(filepath), 'fonts'), {recurse: true});
+    this.changeState('init');
+    this.cachePath = path.join('tmp', this.id);
+    this.beforeState('loaded', this.initCache);
+};
+
+util.inherits(Project, ConfigEmitter);
+
+Project.prototype.load = function (force) {
+    if (this.mml && !force) return this.mml;
+    this.config.log('Loading project from', this.filepath);
+    var ext = path.extname(this.filepath),
+        Loader = this.config.getLoader(ext),
+        loader = new Loader(this);
+    this.mml = loader.load();
+    this.loadTime = Date.now();
+    this.changeState('loaded');
+    return this.mml;
+};
+
+Project.prototype.reload = function () {
+    // TODO Handle concurrency
+    this.xml = null;
+    this.load(true);
+};
+
+Project.prototype.render = function (force) {
+    if (this.xml && !force) return this.xml;
+    this.load(force);
+    var renderer, Renderer;
+    if (this.mml) Renderer = require('./renderer/Carto.js').Carto;
+    else throw 'Oops, unkown renderer';
+    renderer = new Renderer(this);
+    this.config.log('Generating Mapnik XML…');
+    this.xml = renderer.render();
+    return this.xml;
+};
+
+Project.prototype.createMapPool = function (options) {
+    options = options || {};
+    this.render();
+    this.config.log('Loading map…');
+    // TODO bufferSize?
+    this.mapPool = this.mapnikPool.fromString(this.xml, {size: options.size || this.tileSize()}, {base: this.root});
+    this.config.log('Map ready');
+    return this.mapPool;
+};
+
+Project.prototype.export = function (options, callback) {
+    var format = options.format;
+    if (!this.config.exporters[format]) throw 'Unkown format ' + format;
+    var Exporter = require(this.config.exporters[format]).Exporter;
+    var exporter = new Exporter(this, options);
+    exporter.export(callback);
+};
+
+Project.prototype.toFront = function () {
+    var options = {
+        center: [this.mml.center[1], this.mml.center[0]],
+        zoom: this.mml.center[2],
+        minZoom: this.mml.minzoom,
+        maxZoom: this.mml.maxzoom,
+        metatile: this.mml.metatile,
+        name: this.mml.name || '',
+        tileSize: this.tileSize(),
+        loadTime: this.loadTime,
+        layers: this.mml.Layer || []
+    };
+    this.emitAndForward('tofront', {options: options});
+    return options;
+};
+
+Project.prototype.tileSize = function () {
+    return 256 * this.mml.metatile;
+};
+
+Project.prototype.getUrl = function () {
+    return '/' + this.id + '/';
+};
+
+Project.prototype.initCache = function (e) {
+    var self = this, cacheFiles = [];
+    Utils.mkdirs(self.cachePath, function (err) {
+        if (err) throw err;
+        if (self.config.parsed_opts.keepcache) return e.continue();
+        try {
+            cacheFiles = Utils.tree(self.cachePath);
+        } catch (err2) {
+            if (err2 && err2.code !== 'ENOENT') throw err2;
+        }
+        for (var i = 0; i < cacheFiles.length; i++) {
+            if (cacheFiles[i].stat.isFile()) fs.unlink(cacheFiles[i].path);
+        }
+        e.continue();
+    });
+};
+
+exports.Project = Project;
diff --git a/src/back/ProjectServer.js b/src/back/ProjectServer.js
new file mode 100644
index 0000000..da06712
--- /dev/null
+++ b/src/back/ProjectServer.js
@@ -0,0 +1,293 @@
+var fs = require('fs'),
+    path = require('path'),
+    Tile = require('./Tile.js').Tile,
+    GeoUtils = require('./GeoUtils.js'),
+    VectorBasedTile = require('./VectorBasedTile.js').Tile,
+    MetatileBasedTile = require('./MetatileBasedTile.js').Tile,
+    XRayTile = require('./XRayTile.js').Tile;
+var TILEPREFIX = 'tile';
+
+var ProjectServer = function (project, parent) {
+    this.project = project;
+    this.parent = parent;
+    this._pollQueue = [];
+    var self = this;
+    this.project.when('loaded', function () {
+        try {
+            self.initMapPools();
+        } catch (err) {
+            console.log(err.message);
+            self.addToPollQueue({error: err.message});
+        }
+        fs.watch(self.project.root, function (type, filename) {
+            if (filename) {
+                if (filename.indexOf('.') === 0) return;
+                self.project.config.log('File', filename, 'changed on disk');
+            }
+            self.addToPollQueue({isDirty: true});
+        });
+    });
+    this.project.load();
+};
+
+ProjectServer.prototype.serve = function (uri, res) {
+    var urlpath = uri.pathname,
+        els = urlpath.split('/'),
+        self = this;
+    if (!urlpath) this.parent.redirect(this.project.getUrl(), res);
+    else if (urlpath === '/') this.main(res);
+    else if (urlpath === '/config/') this.config(res);
+    else if (urlpath === '/poll/') this.poll(res);
+    else if (urlpath === '/export/') this.export(res, uri.query);
+    else if (urlpath === '/reload/') this.reload(res);
+    else if (this.parent.hasProjectRoute(urlpath)) this.parent.serveProjectRoute(urlpath, uri, res, this.project);
+    else if (els[1] === TILEPREFIX && els.length === 5) this.project.when('loaded', function tile () {self.serveTile(els[2], els[3], els[4], res, uri.query);});
+    else if (els[1] === 'query' && els.length >= 5) this.project.when('loaded', function query () {self.queryTile(els[2], els[3], els[4], res, uri.query);});
+    else this.parent.notFound(urlpath, res);
+};
+
+ProjectServer.prototype.serveTile = function (z, x, y, res, query) {
+    y = y.split('.');
+    var ext = y[1];
+    y = y[0];
+    var func;
+    if (ext === 'json') func = this.jsontile;
+    else if (ext === 'pbf') func = this.pbftile;
+    else if (ext === 'xray') func = this.xraytile;
+    else func = this.tile;
+    try {
+        func.call(this, z, x, y, res, query);
+    } catch (err) {
+        this.raise('Project not loaded properly.', res);
+    }
+};
+
+ProjectServer.prototype.tile = function (z, x, y, res) {
+    var self = this;
+    this.mapPool.acquire(function (err, map) {
+        var release = function () {self.mapPool.release(map);};
+        if (err) return self.raise(err.message, res);
+        var tileClass = self.project.mml.source ? VectorBasedTile : self.project.mml.metatile === 1 ? Tile : MetatileBasedTile;
+        var tile = new tileClass(z, x, y, {width: self.project.tileSize(), height: self.project.tileSize(), metatile: self.project.mml.metatile});
+        return tile.render(self.project, map, function (err, im) {
+            if (err) return self.raise(err.message, res, release);
+            im.encode('png', function (err, buffer) {
+                if (err) return self.raise(err.message, res, release);
+                res.writeHead(200, {'Content-Type': 'image/png', 'Content-Length': buffer.length});
+                res.write(buffer);
+                res.end();
+                release();
+            });
+        });
+    });
+};
+
+ProjectServer.prototype.jsontile = function (z, x, y, res, query) {
+    var self = this;
+    this.vectorMapPool.acquire(function (err, map) {
+        var release = function () {self.vectorMapPool.release(map);};
+        if (err) return self.raise(err.message, res);
+        var tileClass = self.project.mml.source ? VectorBasedTile : Tile;
+        var tile = new tileClass(z, x, y, {metatile: 1});
+        return tile.renderToVector(self.project, map, function (err, tile) {
+            if (err) return self.raise(err.message, res, release);
+            var content;
+            try {
+                content = tile.toGeoJSON(query.layer || '__all__');
+            } catch (err) {
+                // This layer is not visible in this tile,
+                // return an empty geojson;
+                content = '{"type": "FeatureCollection", "features": []}';
+            }
+            if (typeof content !== 'string') content = JSON.stringify(content);  // Mapnik 3.1.0 now returns a string
+            res.writeHead(200, {'Content-Type': 'application/javascript'});
+            res.write(content);
+            res.end();
+            release();
+        });
+    });
+};
+
+ProjectServer.prototype.pbftile = function (z, x, y, res) {
+    var self = this;
+    this.vectorMapPool.acquire(function (err, map) {
+        var release = function () {self.vectorMapPool.release(map);};
+        if (err) return self.raise(err.message, res);
+        var tileClass = self.project.mml.source ? VectorBasedTile : Tile;
+        var tile = new tileClass(z, x, y, {metatile: 1});
+        return tile.renderToVector(self.project, map, function (err, tile) {
+            if (err) return self.raise(err.message, res, release);
+            var content = tile.getData();
+            res.writeHead(200, {'Content-Type': 'application/x-protobuf'});
+            res.write(content);
+            res.end();
+            release();
+        });
+    });
+};
+
+ProjectServer.prototype.xraytile = function (z, x, y, res, query) {
+    var self = this;
+    this.vectorMapPool.acquire(function (err, map) {
+        var release = function () {self.vectorMapPool.release(map);};
+        if (err) return self.raise(err.message, res, release);
+        var tileClass = self.project.mml.source ? VectorBasedTile : Tile;
+        var tile = new tileClass(z, x, y, {metatile: 1, buffer_size: 1});
+        return tile.renderToVector(self.project, map, function (err, t) {
+            if (err) return self.raise(err.message, res, release);
+            var xtile = new XRayTile(z, x, y, t.getData(), {layer: query.layer, background: query.background});
+            xtile.render(self.project, map, function (err, im) {
+                if (err) return self.raise(err.message, res, release);
+                im.encode('png', function (err, buffer) {
+                    if (err) return self.raise(err.message, res, release);
+                    res.writeHead(200, {'Content-Type': 'image/png', 'Content-Length': buffer.length});
+                    res.write(buffer);
+                    res.end();
+                    release();
+                });
+            });
+        });
+    });
+};
+
+ProjectServer.prototype.queryTile = function (z, lat, lon, res, query) {
+    var self = this;
+    lat = parseFloat(lat);
+    lon = parseFloat(lon);
+    this.vectorMapPool.acquire(function (err, map) {
+        var release = function () {self.vectorMapPool.release(map);};
+        var xy = GeoUtils.zoomLatLngToXY(z, lat, lon),
+            x = xy[0], y = xy[1];
+        if (err) return self.raise(err.message, res, release);
+        var tileClass = self.project.mml.source ? VectorBasedTile : Tile;
+        var tile = new tileClass(z, x, y, {metatile: 1});
+        return tile.renderToVector(self.project, map, function (err, t) {
+            if (err) return self.raise(err.message, res, release);
+            var options = {tolerance: parseInt(query.tolerance, 10) || 100};
+            var results = [], layers = [];
+            var doQuery = function (results, options) {
+                var features = t.query(lon, lat, options);
+                for (var i = 0; i < features.length; i++) {
+                    results.push({
+                        distance: features[i].distance,
+                        layer: features[i].layer,
+                        attributes: features[i].attributes()
+                    });
+                }
+            };
+            if (query.layer && query.layer !== '__all__') layers = query.layer.split(',');
+            if (!layers.length) {
+                doQuery(results, options);
+            } else {
+                for (var i = 0; i < layers.length; i++) {
+                    options.layer = layers[i];
+                    doQuery(results, options);
+                }
+            }
+            res.writeHead(200, {'Content-Type': 'application/javascript'});
+            res.write(JSON.stringify(results));
+            res.end();
+            release();
+        });
+    });
+};
+
+ProjectServer.prototype.config = function (res) {
+    res.writeHead(200, {
+        'Content-Type': 'application/javascript'
+    });
+    var tpl = 'L.K.Config.project = %;';
+    res.write(tpl.replace('%', JSON.stringify(this.project.toFront())));
+    res.end();
+};
+
+ProjectServer.prototype.export = function (res, options) {
+    this.project.export(options, function (err, buffer) {
+        if (err) return self.raise(err.message, res);
+        res.writeHead(200, {
+            'Content-Disposition': 'attachment; filename: "xxxx"'
+        });
+        res.write(buffer);
+        res.end();
+    });
+};
+
+ProjectServer.prototype.main = function (res) {
+    var js = this.project.config._js.reduce(function(a, b) {
+        return a + '<script src="' + b + '"></script>\n';
+    }, '');
+    var css = this.project.config._css.reduce(function(a, b) {
+        return a + '<link rel="stylesheet" href="' + b + '" />\n';
+    }, '');
+    fs.readFile(path.join(kosmtik.src, 'front/project.html'), {encoding: 'utf8'}, function(err, data) {
+        if(err) throw err;
+        data = data.replace('%%JS%%', js);
+        data = data.replace('%%CSS%%', css);
+        res.writeHead(200, {
+            'Content-Type': 'text/html',
+            'Content-Length': data.length
+        });
+        res.end(data);
+    });
+};
+
+ProjectServer.prototype.addToPollQueue = function (message) {
+    if (this._pollQueue.indexOf(message) === -1) this._pollQueue.push(message);
+};
+
+ProjectServer.prototype.raise = function (message, res, cb) {
+    console.trace();
+    console.log(message);
+    if (message) this.addToPollQueue({error: message});
+    res.writeHead(500);
+    res.end();
+    if (cb) cb();
+};
+
+ProjectServer.prototype.poll = function (res) {
+    var data = '', len;
+    if (this._pollQueue.length) {
+        data = JSON.stringify(this._pollQueue);
+        this._pollQueue = [];
+    }
+    len = Buffer.byteLength(data, 'utf8');
+    res.writeHead(len ? 200 : 204, {
+        'Content-Type': 'application/json',
+        'Content-Length': len,
+        'Cache-Control': 'private, no-cache, must-revalidate'
+    });
+    res.end(data);
+};
+
+ProjectServer.prototype.reload = function (res) {
+    var self = this;
+    try {
+        this.project.reload();
+    } catch (err) {
+        return this.raise(err.message, res);
+    }
+    this.project.when('loaded', function () {
+        self.mapPool.drain(function() {
+            self.mapPool.destroyAllNow();
+        });
+        self.vectorMapPool.drain(function() {
+            self.vectorMapPool.destroyAllNow();
+        });
+        try {
+            self.initMapPools();
+        } catch (err) {
+            return self.raise(err.message, res);
+        }
+        res.writeHead(200, {
+            'Content-Type': 'application/json'
+        });
+        res.end(JSON.stringify(self.project.toFront()));
+    });
+};
+
+ProjectServer.prototype.initMapPools = function () {
+    this.mapPool = this.project.createMapPool();
+    this.vectorMapPool = this.project.createMapPool({size: 256});
+};
+
+exports.ProjectServer = ProjectServer;
diff --git a/src/back/StateBase.js b/src/back/StateBase.js
new file mode 100644
index 0000000..0a1e215
--- /dev/null
+++ b/src/back/StateBase.js
@@ -0,0 +1,64 @@
+var util = require('util'),
+    events = require('events');
+
+var StateBase = function () {
+    events.EventEmitter.call(this);
+    this._state_before = {};
+    this._state_to_process = {};
+    this._state_after = {};
+    this._state_done = {};
+};
+
+util.inherits(StateBase, events.EventEmitter);
+
+
+StateBase.prototype.beforeState = function (type, listener) {
+    this._state_before[type] = this._state_before[type] || [];
+    this._state_before[type].push(listener);
+};
+
+StateBase.prototype.changeState = function (type, e) {
+
+    this._state_done[type] = false;
+    var done = function () {
+        this._state_done[type] = true;
+        if (this._state_after[type]) {
+            for (var i = 0; i < this._state_after[type].length; i++) {
+                this._state_after[type][i]();
+            }
+        }
+        delete this._state_after[type];
+    }.bind(this);
+    e = e || {};
+    e.continue = function () {
+        if(this._state_to_process[type]) {
+            listeners[listeners.length - this._state_to_process[type]--].call(this, e);
+        } else {
+            done();
+        }
+    }.bind(this);
+    e[this.CLASSNAME] = this;
+    var listeners = this._state_before[type] || [],
+        configType = this.CLASSNAME + ':' + type;
+    if (this.config && this.config._state_before[configType]) listeners = listeners.concat(this.config._state_before[configType] || []);
+    if (!this._state_to_process[type]) {
+        this._state_to_process[type] = listeners.length;
+        e.continue();
+    }
+};
+
+StateBase.prototype.when = function (type, callback) {
+    if (this._state_done[type]) callback();
+    else this.afterState(type, callback);
+};
+
+StateBase.prototype.afterState = function (type, callback) {
+    this._state_after[type] = this._state_after[type] || [];
+    this._state_after[type].push(callback);
+};
+
+exports.StateBase = StateBase;
+
+
+// config.beforeState('project:loaded', myfunc)
+// project.beforeState('loaded', myfunc)
diff --git a/src/back/Tile.js b/src/back/Tile.js
new file mode 100644
index 0000000..e726f1c
--- /dev/null
+++ b/src/back/Tile.js
@@ -0,0 +1,44 @@
+var mapnik = require('mapnik'),
+    zoomXYToLatLng = require('./GeoUtils.js').zoomXYToLatLng;
+
+var Tile = function (z, x, y, options) {
+    options = options || {};
+    var DEFAULT_HEIGHT = 256;
+    var DEFAULT_WIDTH = 256;
+    this.z = +z;
+    this.x = +x;
+    this.y = +y;
+    this.projection = new mapnik.Projection(options.projection || Tile.DEFAULT_OUTPUT_PROJECTION);
+    this.scale = options.scale || 1;
+    this.height = options.height || options.size || DEFAULT_HEIGHT;
+    this.width = options.width || options.size || DEFAULT_WIDTH;
+    this.buffer_size = options.buffer_size || 0;
+};
+
+// 900913
+Tile.DEFAULT_OUTPUT_PROJECTION = '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs +over';
+
+Tile.prototype.setupBounds = function () {
+    var xy = zoomXYToLatLng(this.z, this.x * this.scale, this.y * this.scale);
+    this.maxX = xy[0];
+    this.minY = xy[1];
+    xy = zoomXYToLatLng(this.z, this.x * this.scale + this.scale, this.y * this.scale + this.scale);
+    this.minX = xy[0];
+    this.maxY = xy[1];
+};
+
+Tile.prototype.render = function (project, map, cb) {
+    this.setupBounds();
+    map.zoomToBox(this.projection.forward([this.minX, this.minY, this.maxX, this.maxY]));
+    var im = new mapnik.Image(this.height, this.width);
+    map.render(im, cb);
+};
+
+Tile.prototype.renderToVector = function (project, map, cb) {
+    this.setupBounds();
+    map.zoomToBox(this.projection.forward([this.minX, this.minY, this.maxX, this.maxY]));
+    var surface = new mapnik.VectorTile(this.z, this.x, this.y);
+    map.render(surface, {buffer_size: this.buffer_size}, cb);
+};
+
+exports.Tile = Tile;
diff --git a/src/back/Utils.js b/src/back/Utils.js
new file mode 100644
index 0000000..588715b
--- /dev/null
+++ b/src/back/Utils.js
@@ -0,0 +1,43 @@
+var fs = require('fs'),
+    path = require('path');
+
+module.exports = {
+
+    mkdirs: function (dirpath, callback) {
+        fs.mkdir(dirpath, function (err) {
+            if (err && err.code === 'ENOENT') module.exports.mkdirs(path.dirname(dirpath), function () {module.exports.mkdirs(dirpath, callback);});
+            else if (err && err.code !== 'EEXIST') callback(err);
+            else callback();
+        });
+    },
+
+    sinh: function (x) {
+        var y = Math.exp(x);
+        return (y - 1/y) / 2;
+    },
+
+    template: function (str, data) {
+        return str.replace(/\{ *([\w_]+) *\}/g, function (str, key) {
+            var value = data[key];
+            if (value === undefined) {
+                throw new Error('No value provided for variable ' + str);
+            } else if (typeof value === 'function') {
+                value = value(data);
+            }
+            return value;
+        });
+    },
+
+    tree: function(dir) {
+        var results = [];
+        var list = fs.readdirSync(dir);
+        list.forEach(function(file) {
+            file = path.join(dir, file);
+            var stat = fs.statSync(file);
+            results.push({path: file, stat: stat});
+            if (stat && stat.isDirectory()) results = results.concat(module.exports.tree(file));
+        });
+        return results;
+    }
+
+};
diff --git a/src/back/VectorBasedTile.js b/src/back/VectorBasedTile.js
new file mode 100644
index 0000000..be495f6
--- /dev/null
+++ b/src/back/VectorBasedTile.js
@@ -0,0 +1,73 @@
+var util = require('util'),
+    mapnik = require('mapnik'),
+    Tile = require('./Tile.js').Tile,
+    Utils = require('./Utils.js'),
+    zlib = require('zlib');
+
+var VectorBasedTile = function (z, x, y, options) {
+    Tile.call(this, z, x, y, options);
+};
+
+util.inherits(VectorBasedTile, Tile);
+
+VectorBasedTile.prototype._render = function (project, map, cb) {
+    this.setupBounds();
+    map.zoomToBox(this.projection.forward([this.minX, this.minY, this.maxX, this.maxY]));
+    var vtile = new mapnik.VectorTile(this.z, this.x, this.y),
+        processed = 0,
+        parse = function (data, resp) {
+            try {
+                vtile.setData(data);
+                vtile.parse();
+            } catch (error) {
+                console.log(error.message);
+                return cb(new Error('Unable to parse vector tile data for uri ' + resp.request.uri.href));
+            }
+            if (++processed === project.mml.source.length) cb(null, vtile);
+        },
+        onResponse = function (err, resp, body) {
+            if (err) return cb(err);
+            if (resp.statusCode !== 200) return cb(new Error('Unable to retrieve data from ' + resp.request.uri.href));
+            var compression = false;
+            if (resp.headers['content-encoding'] === 'gzip') compression = 'gunzip';
+            else if (resp.headers['content-encoding'] === 'deflate') compression = 'inflate';
+            else if (body && body[0] === 0x1F && body[1] === 0x8B) compression = 'gunzip';
+            else if (body && body[0] === 0x78 && body[1] === 0x9C) compression = 'inflate';
+            if (compression) {
+                zlib[compression](body, function(err, data) {
+                    if (err) return cb(err);
+                    parse(data, resp);
+                });
+            } else {
+                parse(body, resp);
+            }
+        },
+        params = {
+            z: this.z,
+            x: this.x,
+            y: this.y
+        };
+    for (var i = 0; i < project.mml.source.length; i++) {
+        var options = {
+            uri: Utils.template(project.mml.source[i].url, params),
+            encoding: null  // we want a buffer, not a string
+        };
+        project.config.helpers.request(options, onResponse);
+    }
+};
+
+VectorBasedTile.prototype.render = function (project, map, cb) {
+    var self = this;
+    this._render(project, map, function (err, vtile) {
+        if (err) cb(err);
+        else vtile.render(map, new mapnik.Image(self.width, self.height), cb);
+    });
+};
+
+
+VectorBasedTile.prototype.renderToVector = function (project, map, cb) {
+    this._render(project, map, cb);
+};
+
+
+exports.Tile = VectorBasedTile;
diff --git a/src/back/XRayTile.js b/src/back/XRayTile.js
new file mode 100644
index 0000000..c7e2770
--- /dev/null
+++ b/src/back/XRayTile.js
@@ -0,0 +1,52 @@
+var mapnik = require('mapnik'),
+    Utils = require('./Utils.js'),
+    fs = require('fs'),
+    path = require('path'),
+    crypto = require('crypto');
+
+var XRayTile = function (z, x, y, data, options) {
+    this.z = +z;
+    this.x = +x;
+    this.y = +y;
+    this.data = data;
+    this.options = options || {};
+};
+
+XRayTile.prototype.render = function (project, map, cb) {
+    var styleMap = this.styleMap(project),
+        vtile = new mapnik.VectorTile(this.z, this.x, this.y);
+    if (this.data.length){
+        vtile.setData(this.data);
+        vtile.parse();
+    }
+    vtile.render(styleMap, new mapnik.Image(256, 256), cb);
+};
+
+XRayTile.prototype.styleMap = function (project) {
+    var self = this,
+        map = new mapnik.Map(256, 256),
+        idx = 0,
+        chosenLayers = (self.options.layer) ? self.options.layer.split(',') : [],
+        layers = project.mml.Layer.reduce(function (prev, layer) {
+            if (chosenLayers.length && chosenLayers.indexOf(layer.id) === -1) return prev;
+            if (idx >= XRayTile.colors.length) idx = 0;
+            return prev + Utils.template(XRayTile.layerTemplate, {id: layer.id, rgb: XRayTile.colors[idx++]});
+        }, ''),
+        xml = Utils.template(XRayTile.mapTemplate, {layers: layers || '', bg: this.options.background || '#000000'});
+    map.fromStringSync(xml);
+    return map;
+};
+
+XRayTile.prototype.stringToRGB = function (s) {
+    var hash = crypto.createHash('md5').update(s).digest('hex').slice(0, 3);
+    return [hash.charCodeAt(0) + 100, hash.charCodeAt(1) + 100, hash.charCodeAt(2) + 100].join(',');
+};
+
+XRayTile.mapTemplate = fs.readFileSync(path.join(__dirname, 'xray', 'map.xml'), 'utf8');
+XRayTile.layerTemplate = fs.readFileSync(path.join(__dirname, 'xray', 'layer.xml'), 'utf8');
+XRayTile.colors = [
+    '218,223,225', '217,30,24', '102,51,153', '68,108,179', '247,202,24', '38,166,91', '78,205,196', '219,10,91', '232,126,4', '135,211,124'
+];
+
+
+exports.Tile = XRayTile;
diff --git a/src/back/loader/Base.js b/src/back/loader/Base.js
new file mode 100644
index 0000000..3839111
--- /dev/null
+++ b/src/back/loader/Base.js
@@ -0,0 +1,79 @@
+var fs = require('fs'),
+    path = require('path'),
+    url = require('url');
+
+var BaseLoader = function (project) {
+    this.project = project;
+};
+
+BaseLoader.prototype.postprocess = function () {
+    this.mml.metatile = +(this.mml.metatile || this.mml.source ? 1 : 2);  // Default vectortiles to 1, classic to 2.
+    if (this.mml.Stylesheet) {
+        this.mml.Stylesheet = this.mml.Stylesheet.map(this.normalizeStylesheet.bind(this));
+    }
+    if (this.mml.styles) {
+        this.mml.Stylesheet = (this.mml.Stylesheet || []).concat(this.mml.styles.map(this.normalizeStylesheet.bind(this)));
+    }
+    if (!this.mml.Layer) this.mml.Layer = [];
+    if (this.mml.layers) {
+        this.mml.Layer = (this.mml.Layer || []).concat(this.mml.layers.map(this.expandLayerName.bind(this)));
+    }
+    this.mml.Layer = this.mml.Layer.map(this.normalizeLayer);
+    if (this.mml.source) {
+        if (typeof this.mml.source === 'string') this.mml.source = this.mml.source.split(this.mml.source_separator || ',');
+        this.mml.source = this.mml.source.map(this.normalizeSource);
+    }
+    // Do not hardcode me, hombre!
+    if (!this.mml.srs) this.mml.srs = '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over';
+};
+
+BaseLoader.prototype.normalizeLayer = function (layer) {
+    if (!layer.srs) layer.srs = this.srs;
+    if (!layer.name) layer.name = layer.id;
+    return layer;
+};
+
+BaseLoader.prototype.normalizeSource = function (source) {
+    if (typeof source === 'string') {
+        var uri = url.parse(source);
+        // Since 0.12, url.parse escapes unwise chars in URL, but we need
+        // to keep the variables, like {x}, {y} as is.
+        uri.href = uri.href.replace(/%7B/g, '{').replace(/%7D/g, '}');
+        source = {
+            protocol: uri.protocol
+        };
+        if (source.protocol === 'tmsource:') {
+            source.path = uri.path;
+        } else if (source.protocol.indexOf('http') === 0) {
+            source.tilejson = uri.href;
+        } else if (source.protocol.indexOf('tms') === 0) {
+            source.url = uri.href.replace(/^tms/, 'http');
+        }
+    }
+    return source;
+};
+
+BaseLoader.prototype.expandLayerName = function (name) {
+    var className = '';
+    if (name.indexOf('.') !== -1) {
+        var els = name.split('.');
+        name = els[0];
+        className = els[1];
+    }
+    return {id: name, 'class': className};
+};
+
+BaseLoader.prototype.normalizeStylesheet = function (style) {
+    if (typeof style !== 'string') {
+        return { id: style.id, data: style.data };
+    }
+    return { id: style, data: fs.readFileSync(path.join(this.project.root, style), 'utf8') };
+};
+
+BaseLoader.prototype.load = function () {
+    this.mml = this.loadFile();
+    this.postprocess();
+    return this.mml;
+};
+
+exports.BaseLoader = BaseLoader;
diff --git a/src/back/loader/MML.js b/src/back/loader/MML.js
new file mode 100644
index 0000000..7e50f3d
--- /dev/null
+++ b/src/back/loader/MML.js
@@ -0,0 +1,14 @@
+var fs = require('fs'),
+    util = require('util'),
+    BaseLoader = require('./Base.js').BaseLoader;
+
+var Loader = function (project) {
+    BaseLoader.call(this, project);
+};
+util.inherits(Loader, BaseLoader);
+
+Loader.prototype.loadFile = function () {
+    return JSON.parse(fs.readFileSync(this.project.filepath, 'utf8'));
+};
+
+exports.Loader = Loader;
diff --git a/src/back/loader/YAML.js b/src/back/loader/YAML.js
new file mode 100644
index 0000000..358b724
--- /dev/null
+++ b/src/back/loader/YAML.js
@@ -0,0 +1,15 @@
+var fs = require('fs'),
+    util = require('util'),
+    yaml = require('js-yaml'),
+    BaseLoader = require('./Base.js').BaseLoader;
+
+var Loader = function (project) {
+    BaseLoader.call(this, project);
+};
+util.inherits(Loader, BaseLoader);
+
+Loader.prototype.loadFile = function () {
+    return yaml.safeLoad(fs.readFileSync(this.project.filepath, 'utf8'));
+};
+
+exports.Loader = Loader;
diff --git a/src/back/renderer/Carto.js b/src/back/renderer/Carto.js
new file mode 100644
index 0000000..dd86e6e
--- /dev/null
+++ b/src/back/renderer/Carto.js
@@ -0,0 +1,24 @@
+var carto = require('carto');
+
+
+var Carto = function (project) {
+    this.project = project;
+};
+
+Carto.prototype.render = function () {
+    var env = {
+            filename: this.project.filepath,
+            local_data_dir: this.project.root,
+            validation_data: { fonts: this.project.mapnik.fonts() },
+            returnErrors: true,
+            effects: []
+        },
+        options = {
+            mapnik_version: this.project.mml.mapnik_version || this.project.config.parsed_opts.mapnik_version
+        };
+    this.project.config.log('Using mapnik version', options.mapnik_version);
+    return new carto.Renderer(env, options).render(this.project.mml);
+
+};
+
+exports.Carto = Carto;
diff --git a/src/back/xray/layer.xml b/src/back/xray/layer.xml
new file mode 100644
index 0000000..830f736
--- /dev/null
+++ b/src/back/xray/layer.xml
@@ -0,0 +1,24 @@
+<Style name="{id}" filter-mode="first" comp-op="screen">
+  <Rule>
+    <Filter>([mapnik::geometry_type] = 3)</Filter>
+    <LineSymbolizer stroke-width="0.5" stroke="rgba({rgb},0.5)" />
+    <PolygonSymbolizer fill="rgba({rgb},0.05)" />
+  </Rule>
+  <Rule>
+    <Filter>([mapnik::geometry_type] = 2)</Filter>
+    <LineSymbolizer stroke-width="1" stroke="rgba({rgb},0.5)" />
+  </Rule>
+  <Rule>
+    <Filter>([mapnik::geometry_type] = 1)</Filter>
+    <MarkersSymbolizer allow-overlap="true" width="4" height="4" stroke-width="0" stroke="#ffffff" fill="rgba({rgb},0.5)" />
+    <TextSymbolizer allow-overlap="true" justify-alignment="left" face-name="DejaVu Sans Book" fill="rgba({rgb},0.75)" size="12" dy="6" ><![CDATA[[name]]]></TextSymbolizer>
+  </Rule>
+  <Rule>
+    <ElseFilter></ElseFilter>
+    <RasterSymbolizer opacity="1" />
+  </Rule>
+</Style>
+
+<Layer name="{id}" srs="+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over">
+    <StyleName>{id}</StyleName>
+</Layer>
diff --git a/src/back/xray/map.xml b/src/back/xray/map.xml
new file mode 100644
index 0000000..042c668
--- /dev/null
+++ b/src/back/xray/map.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE Map[]>
+<Map srs="+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over" background-color="{bg}" maximum-extent="-20037508.34,-20037508.34,20037508.34,20037508.34">
+
+<Parameters>
+  <Parameter name="bounds">-180,-85.0511,180,85.0511</Parameter>
+  <Parameter name="center">0,0,3</Parameter>
+  <Parameter name="format">png8:m=h</Parameter>
+  <Parameter name="maxzoom">22</Parameter>
+  <Parameter name="minzoom">0</Parameter>
+  <Parameter name="scale">1</Parameter>
+</Parameters>
+
+{layers}
+
+<Style name="_image" filter-mode="first">
+  <Rule>
+    <RasterSymbolizer opacity="1" />
+  </Rule>
+</Style>
+<Layer name="_image" srs="+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over">
+  <StyleName>_image</StyleName>
+</Layer>
+
+</Map>
diff --git a/src/front/Autocomplete.js b/src/front/Autocomplete.js
new file mode 100644
index 0000000..381dae7
--- /dev/null
+++ b/src/front/Autocomplete.js
@@ -0,0 +1,296 @@
+
+L.Kosmtik.Autocomplete = L.Evented.extend({
+
+    options: {
+        placeholder: 'Start typing…',
+        emptyMessage: 'No result',
+        minChar: 3,
+        submitDelay: 300
+    },
+
+    CACHE: '',
+    RESULTS: [],
+    KEYS: {
+        LEFT: 37,
+        UP: 38,
+        RIGHT: 39,
+        DOWN: 40,
+        TAB: 9,
+        RETURN: 13,
+        ESC: 27,
+        APPLE: 91,
+        SHIFT: 16,
+        ALT: 17,
+        CTRL: 18
+    },
+
+    initialize: function (container, options) {
+        this.container = container;
+        L.setOptions(this, options);
+
+        this.options = L.Util.extend(this.options, options);
+        var CURRENT = null;
+
+        try {
+            Object.defineProperty(this, 'CURRENT', {
+                get: function () {
+                    return CURRENT;
+                },
+                set: function (index) {
+                    if (typeof index === 'object') {
+                        index = this.resultToIndex(index);
+                    }
+                    CURRENT = index;
+                }
+            });
+        } catch (e) {
+            // Hello IE8 and monsters
+        }
+
+        this.createInput();
+        this.createResultsContainer();
+    },
+
+    createInput: function () {
+        this.input = L.DomUtil.create('input', 'k-autcomplete-input', this.container);
+        this.input.type = 'text';
+        this.input.placeholder = this.options.placeholder;
+        this.input.autocomplete = 'off';
+        L.DomEvent.disableClickPropagation(this.input);
+
+        L.DomEvent.on(this.input, 'keydown', this.onKeyDown, this);
+        L.DomEvent.on(this.input, 'keyup', this.onKeyUp, this);
+        L.DomEvent.on(this.input, 'blur', this.onBlur, this);
+        L.DomEvent.on(this.input, 'focus', this.onFocus, this);
+    },
+
+    createResultsContainer: function () {
+        this.resultsContainer = L.DomUtil.create('ul', 'k-autocomplete-results', document.querySelector('body'));
+    },
+
+    resizeContainer: function() {
+        var l = this.getLeft(this.input);
+        var t = this.getTop(this.input) + this.input.offsetHeight;
+        this.resultsContainer.style.left = l + 'px';
+        this.resultsContainer.style.top = t + 'px';
+        var width = this.options.width ? this.options.width : this.input.offsetWidth - 2;
+        this.resultsContainer.style.width = width + 'px';
+    },
+
+    onKeyDown: function (e) {
+        switch (e.keyCode) {
+            case this.KEYS.TAB:
+                if(this.CURRENT !== null)
+                {
+                    this.setChoice();
+                }
+                L.DomEvent.stop(e);
+                break;
+            case this.KEYS.RETURN:
+                L.DomEvent.stop(e);
+                this.setChoice();
+                break;
+            case this.KEYS.ESC:
+                L.DomEvent.stop(e);
+                this.hide();
+                this.input.blur();
+                break;
+            case this.KEYS.DOWN:
+                if(this.RESULTS.length > 0) {
+                    if(this.CURRENT !== null && this.CURRENT < this.RESULTS.length - 1) {
+                        this.CURRENT++;
+                        this.highlight();
+                    }
+                    else if(this.CURRENT === null) {
+                        this.CURRENT = 0;
+                        this.highlight();
+                    }
+                }
+                L.DomEvent.stop(e);
+                break;
+            case this.KEYS.UP:
+                if(this.CURRENT !== null) {
+                    L.DomEvent.stop(e);
+                }
+                if(this.RESULTS.length > 0) {
+                    if(this.CURRENT > 0) {
+                        this.CURRENT--;
+                        this.highlight();
+                    }
+                    else if(this.CURRENT === 0) {
+                        this.CURRENT = null;
+                        this.highlight();
+                    }
+                }
+                break;
+        }
+    },
+
+    onKeyUp: function (e) {
+        var special = [
+            this.KEYS.TAB,
+            this.KEYS.RETURN,
+            this.KEYS.LEFT,
+            this.KEYS.RIGHT,
+            this.KEYS.DOWN,
+            this.KEYS.UP,
+            this.KEYS.APPLE,
+            this.KEYS.SHIFT,
+            this.KEYS.ALT,
+            this.KEYS.CTRL
+        ];
+        if (special.indexOf(e.keyCode) === -1)
+        {
+            if (typeof this.submitDelay === 'number') {
+                window.clearTimeout(this.submitDelay);
+                delete this.submitDelay;
+            }
+            this.submitDelay = window.setTimeout(L.Util.bind(this.typeahead, this), this.options.submitDelay);
+        }
+    },
+
+    onBlur: function (e) {
+        this.fire('blur');
+        var self = this;
+        setTimeout(function () {
+            self.hide();
+        }, 100);
+    },
+
+    onFocus: function (e) {
+        this.fire('focus');
+        this.input.select();
+    },
+
+    clear: function () {
+        this.RESULTS = [];
+        this.CURRENT = null;
+        this.CACHE = '';
+        this.resultsContainer.innerHTML = '';
+    },
+
+    hide: function() {
+        this.fire('hide');
+        this.clear();
+        this.resultsContainer.style.display = 'none';
+    },
+
+    setChoice: function (choice) {
+        choice = choice || this.RESULTS[this.CURRENT];
+        if (choice) {
+            this.hide();
+            this.input.value = '';
+            this.fire('selected', {choice: choice});
+            if (this.options.callback) this.options.callback(choice);
+        }
+    },
+
+    typeahead: function() {
+        var val = this.input.value;
+        if (val.length < this.options.minChar) {
+            this.clear();
+            return;
+        }
+        if(!val) {
+            this.clear();
+            return;
+        }
+        if( val + '' === this.CACHE + '') {
+            return;
+        }
+        else {
+            this.CACHE = val;
+        }
+        this.fire('typeahead', {value: val});
+    },
+
+    _formatResult: function (item, el) {
+        var name = L.DomUtil.create('strong', '', el),
+            detailsContainer = L.DomUtil.create('small', '', el);
+        name.innerHTML = item.name;
+        if (item.description) detailsContainer.innerHTML = item.description;
+    },
+
+    formatResult: function (item, el) {
+        return (this.options.formatResult || this._formatResult).call(this, item, el);
+    },
+
+    createResult: function (item, index) {
+        var el = L.DomUtil.create('li', '', this.resultsContainer);
+        if (!item.html) this.formatResult(item, el);
+        else el.innerHTML = el.html;
+        item.el = el;
+        L.DomEvent.on(el, 'mouseover', function (e) {
+            this.CURRENT = index;
+            this.highlight();
+        }, this);
+        L.DomEvent.on(el, 'mousedown', function (e) {
+            this.setChoice();
+        }, this);
+        return item;
+    },
+
+    resultToIndex: function (result) {
+        var out = null;
+        this.forEach(this.RESULTS, function (item, index) {
+            if (item === result) {
+                out = index;
+                return;
+            }
+        });
+        return out;
+    },
+
+    handleResults: function(items) {
+        var self = this;
+        this.clear();
+        this.resultsContainer.style.display = 'block';
+        this.resizeContainer();
+        this.forEach(items, function (item, index) {
+            self.RESULTS.push(self.createResult(item, index));
+        });
+        if (items.length === 0) {
+            var noresult = L.DomUtil.create('li', 'k-autocomplete-no-result', this.resultsContainer);
+            noresult.innerHTML = this.options.emptyMessage;
+        }
+        this.CURRENT = 0;
+        this.highlight();
+    },
+
+    highlight: function () {
+        var self = this;
+        this.forEach(this.RESULTS, function (item, index) {
+            if (index === self.CURRENT) {
+                L.DomUtil.addClass(item.el, 'on');
+            }
+            else {
+                L.DomUtil.removeClass(item.el, 'on');
+            }
+        });
+    },
+
+    getLeft: function (el) {
+        var tmp = el.offsetLeft;
+        el = el.offsetParent;
+        while(el) {
+            tmp += el.offsetLeft;
+            el = el.offsetParent;
+        }
+        return tmp;
+    },
+
+    getTop: function (el) {
+        var tmp = el.offsetTop;
+        el = el.offsetParent;
+        while(el) {
+            tmp += el.offsetTop;
+            el = el.offsetParent;
+        }
+        return tmp;
+    },
+
+    forEach: function (els, callback) {
+        Array.prototype.forEach.call(els, callback);
+    }
+
+});
diff --git a/src/front/Command.js b/src/front/Command.js
new file mode 100644
index 0000000..782c071
--- /dev/null
+++ b/src/front/Command.js
@@ -0,0 +1,127 @@
+L.Kosmtik.Command = L.Class.extend({
+
+    initialize: function (map, options) {
+        this._map = map;
+        L.setOptions(this, options);
+        L.DomEvent.addListener(document, 'keydown', this.onKeyDown, this);
+        this._specs = [];
+        this._listeners = {};
+        this.tool = L.DomUtil.create('span', 'k-command-palette');
+        var formatResult = function (spec, el) {
+            var name = L.DomUtil.create('strong', '', el);
+            name.innerHTML = spec.name;
+            if (spec.description) name.title = spec.description;
+            if (spec.keyCode) {
+                var key = L.K.Command.makeLabel(spec),
+                    shortcut = L.DomUtil.create('small', 'shortcut');
+                shortcut.innerHTML = key;
+                el.insertBefore(shortcut, el.firstChild);
+            }
+        };
+        this.autocomplete = new L.K.Autocomplete(this.tool, {
+            minChar: 0,
+            placeholder: 'Type command (ctrl-shift-P)…',
+            emptyMessage: 'No matching command',
+            formatResult: formatResult,
+            submitDelay: 100
+        });
+        map.toolbar.addTool(this.tool);
+        this.autocomplete.on('typeahead', function (e) {
+            var results = this.scoreAll(e.value).slice(0, 10);
+            this.autocomplete.handleResults(results);
+        }, this);
+        this.autocomplete.on('selected', function (e) {
+            e.choice.callback.apply(e.choice.context || this._map);
+        }, this);
+        this.add({
+            keyCode: L.K.Keys.P,
+            shiftKey: true,
+            ctrlKey: true,
+            callback: this.focus,
+            context: this,
+            name: 'Command palette: focus'
+        });
+    },
+
+    _makeKey: function (e) {
+        var els = [e.keyCode];
+        if (e.altKey) els.push('alt');
+        if (e.ctrlKey) els.push('ctrl');
+        if (e.shiftKey) els.push('shift');
+        return els.join('.');
+    },
+
+    add: function (specs) {
+        if (typeof specs.keyCode === 'string') specs.keyCode = L.K[specs.keyCode.upper()];
+        if (!specs.callback) return console.error('Missing callback in command specs', specs);
+        if (specs.keyCode) this._listeners[this._makeKey(specs)] = specs;
+        this._specs.push(specs);
+    },
+
+    remove: function (specs) {
+        var key = this._makeKey(specs);
+        delete this._listeners[key];
+    },
+
+    onKeyDown: function (e) {
+        var key = this._makeKey(e),
+            specs = this._listeners[key];
+        if (specs) {
+            if(specs.stop !== false) L.DomEvent.stop(e);
+            specs.callback.apply(specs.context || this._map);
+        }
+    },
+
+    each: function (method, context) {
+        for (var i = 0; i < this._specs.length; i++) {
+            method.call(context, this._specs[i]);
+        }
+        return this;
+    },
+
+    filter: function (filter, max) {
+        max = max || 10;
+        var specs = [], spec;
+        for (var i = 0; i < this._specs.length; i++) {
+            if (specs.length >= max) break;
+            spec = this._specs[i];
+            if (spec.name && spec.name.toString().toLowerCase().indexOf(filter) !== -1) specs.push(spec);
+        }
+        return specs;
+    },
+
+    score: function (spec, query) {
+        query = query.toLowerCase();
+        var name = (spec.name || '').toString().toLowerCase(),
+            index = name.indexOf(query);
+        if (index === 0) spec.score = 5;
+        else if (index > 0) spec.score = 3;
+        else if (name.search(query.split('').join('.*')) !== -1) spec.score = 1;
+        else spec.score = 0;
+    },
+
+    scoreAll: function (query) {
+        var match = [];
+        this.each(function (spec) {
+            this.score(spec, query);
+            if (spec.score > 0) match.push(spec);
+        }, this);
+        return match.sort(function (a, b) {
+            return b.score - a.score;
+        });
+    },
+
+    focus: function () {
+        this.autocomplete.input.focus();
+    }
+
+});
+
+L.Kosmtik.Command.makeLabel = function (e) {
+    var els = [];
+    if (e.altKey) els.push('alt');
+    if (e.ctrlKey) els.push('ctrl');
+    if (e.shiftKey) els.push('shift');
+    els.push(L.K.KeysLabel[e.keyCode]);
+    return els.join('+');
+};
diff --git a/src/front/Core.css b/src/front/Core.css
new file mode 100644
index 0000000..1479ebb
--- /dev/null
+++ b/src/front/Core.css
@@ -0,0 +1,632 @@
+/* ************************************************* */
+/* *********************** FONT ******************** */
+/* ************************************************* */
+
+
+ at font-face {
+    font-family: 'fira_sansbold';
+    src: url('./fonts/FiraSans-Bold.eot');
+    src: url('./fonts/FiraSans-Bold.eot?#iefix') format('embedded-opentype'),
+         url('./fonts/FiraSans-Bold.woff') format('woff'),
+         url('./fonts/FiraSans-Bold.ttf') format('truetype');
+    font-weight: normal;
+    font-style: normal;
+
+}
+
+
+ at font-face {
+    font-family: 'fira_sansregular';
+    src: url('./fonts/FiraSans-Regular.eot');
+    src: url('./fonts/FiraSans-Regular.eot?#iefix') format('embedded-opentype'),
+         url('./fonts/FiraSans-Regular.woff') format('woff'),
+         url('./fonts/FiraSans-Regular.ttf') format('truetype');
+    font-weight: normal;
+    font-style: normal;
+}
+
+ at font-face {
+    font-family: 'fira_sanslight';
+    src: url('./fonts/FiraSans-Light.eot');
+    src: url('./fonts/FiraSans-Light.eot?#iefix') format('embedded-opentype'),
+         url('./fonts/FiraSans-Light.woff') format('woff'),
+         url('./fonts/FiraSans-Light.ttf') format('truetype');
+    font-weight: normal;
+    font-style: normal;
+}
+
+ at font-face {
+    font-family: 'dejavu_sansbook';
+    src: url('./fonts/DejaVuSans-webfont.eot');
+    src: url('./fonts/DejaVuSans-webfont.eot?#iefix') format('embedded-opentype'),
+         url('./fonts/DejaVuSans-webfont.woff') format('woff'),
+         url('./fonts/DejaVuSans-webfont.ttf') format('truetype');
+    font-weight: normal;
+    font-style: normal;
+
+}
+
+
+/* *********** */
+/*   generic   */
+/* *********** */
+body, div, ul, li, a, section, nav,
+h1, h2, h3, h4, h5, h6,
+hr, input, textarea {
+    -moz-box-sizing: border-box;
+    -webkit-box-sizing: border-box;
+    box-sizing: border-box;
+    margin: 0;
+    padding: 0;
+    font-family: 'fira_sanslight', sans-serif;
+}
+ul {
+    list-style-image:none;
+    list-style-position:inside;
+    list-style-type:none;
+}
+
+/* *********** */
+/*    forms    */
+/* *********** */
+input[type="text"], input[type="password"], input[type="date"],
+input[type="datetime"], input[type="email"], input[type="number"],
+input[type="search"], input[type="tel"], input[type="time"],
+input[type="url"], textarea {
+    background-color: #ecf0f1;
+    border: 1px solid #CCCCCC;
+    border-radius: 2px 2px 2px 2px;
+    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) inset;
+    color: rgba(0, 0, 0, 0.75);
+    display: block;
+    font-family: inherit;
+    font-size: 14px;
+    height: 56px;
+    margin: 0 0 14px;
+    padding: 7px;
+    width: 100%;
+}
+textarea {
+    height: inherit;
+}
+select {
+    width: 100%;
+    background-color: #ecf0f1;
+    height: 28px;
+    line-height: 28px;
+    color: rgba(0, 0, 0, 0.75);
+    border: 1px solid #ddd;
+}
+select[multiple="multiple"] {
+    height: auto;
+}
+.button, input[type="submit"] {
+    display: block;
+    width: 100%;
+    background-color: #2c3e50;
+    color: #fff;
+    border: none;
+    margin-bottom: 14px;
+    text-align: center;
+    min-height: 56px;
+    line-height: 56px;
+    border-radius: 2px;
+    font-weight: normal;
+    cursor: pointer;
+}
+.button:hover, input[type="submit"]:hover {
+    background-color: #34495e;
+}
+.help-text, .helptext {
+    display: block;
+    padding: 7px 7px;
+    margin-top: -14px;
+    margin-bottom: 14px;
+    background: #777;
+    color: #eee;
+    font-size: 11px;
+    border-radius: 0 2px;
+}
+select + .help-text {
+    margin-top: 0;
+}
+.formbox {
+    background-color: #555;
+    min-height: 28px;
+    line-height: 28px;
+    padding-left: 14px;
+    margin-bottom: 14px;
+}
+.formbox select {
+    width: calc(100% - 14px);
+}
+.formbox.with-switch {
+    padding-top: 2px;
+}
+label {
+    display: block;
+    font-size: 12px;
+    line-height: 21px;
+    width: 100%;
+}
+input[type="checkbox"] + label, input[type="radio"] + label {
+    display: inline;
+    padding: 0 14px;
+}
+select + .error,
+input + .error {
+    display: block;
+    padding: 7px 7px;
+    margin-top: -14px;
+    margin-bottom: 14px;
+    background: #ddd;
+    color: #fff;
+    background-color: #cc0000;
+    font-size: 11px;
+    border-radius: 0 2px;
+}
+input[type="file"] + .error {
+    margin-top: 0;
+}
+fieldset.toggle > * {
+    display: none;
+}
+fieldset.toggle {
+    border-top: 1px solid #999;
+    border-bottom: 1px solid #ddd;
+    border-left: 1px solid #ddd;
+    border-right: 1px solid #ddd;
+    padding: 0 10px;
+}
+fieldset.toggle.on {
+    border: 1px solid #999;
+    padding-bottom: 5px;
+}
+
+fieldset.toggle .more_style_options,
+fieldset.toggle legend {
+    display: block;
+    cursor: pointer;
+}
+fieldset.toggle .more_style_options:before {
+    content: "…";
+}
+fieldset.toggle legend:before {
+    content: "▶";
+    padding-right: 5px;
+    color: #666;
+    font-size: 0.9em;
+    vertical-align: middle;
+}
+fieldset.toggle.on legend:before {
+    content: "▼";
+}
+fieldset.toggle.on > * {
+    display: block;
+}
+fieldset.toggle.on .more_style_options {
+    display: none;
+}
+
+/* Switch */
+input.switch:empty {
+    display: none;
+}
+input.switch:empty ~ label {
+    position: relative;
+    float: left;
+    line-height: 1.6em;
+    text-indent: 3em;
+    margin: 0.2em 0;
+    cursor: pointer;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+}
+input.switch:empty ~ label:before,
+input.switch:empty ~ label:after {
+    position: absolute;
+    display: block;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    content: ' ';
+    width: 3.6em;
+    background-color: #8A4740;
+    -webkit-transition: all 100ms ease-in;
+    transition: all 100ms ease-in;
+    color: #efefef;
+}
+input.switch:empty ~ label:after {
+    width: 1.4em;
+    top: 0.1em;
+    bottom: 0.1em;
+    margin-left: 0.1em;
+    background-color: #efefef;
+    content: "off";
+    text-indent: 22px;
+}
+input.switch:checked:empty ~ label:after {
+    content: ' ';
+}
+input.switch:checked ~ label:before {
+    background-color: #3E815A;
+    content: "on";
+    text-indent: 4px;
+    text-align: left;
+}
+input.switch:checked ~ label:after {
+    margin-left: 2.1em;
+}
+
+
+
+
+/* *********** */
+/*     Map     */
+/* *********** */
+#map {
+    position: absolute;
+    top: 40px;
+    bottom: 0;
+    right: 0;
+    left: 0;
+    width:100%;
+}
+.zoom-indicator {
+    background-color: #444;
+    color: #ecf0f1;
+    text-align: center;
+    font-family: 'fira_sanslight';
+}
+
+
+/* ********************************* */
+/*         Ajax loader               */
+/* ********************************* */
+.map-loader
+{
+    position: absolute;
+    display: none;
+    top: 0;
+    left: 0;
+    right: 0;
+    height: 3px;
+    z-index: 10100;
+    background-color: #d35400;
+    -webkit-transform: translateX(100%);
+    -moz-transform: translateX(100%);
+    -o-transform: translateX(100%);
+    transform: translateX(100%);
+}
+.loading .map-loader
+{
+    display: block;
+    -webkit-animation: shift-rightwards 3s ease-in-out infinite;
+    -moz-animation: shift-rightwards 3s ease-in-out infinite;
+    -ms-animation: shift-rightwards 3s ease-in-out infinite;
+    -o-animation: shift-rightwards 3s ease-in-out infinite;
+    animation: shift-rightwards 3s ease-in-out infinite;
+    -webkit-animation-delay: .2s;
+    -moz-animation-delay: .2s;
+    -o-animation-delay: .2s;
+    animation-delay: .2s;
+}
+
+ at -webkit-keyframes shift-rightwards
+{
+    0%
+    {
+        -webkit-transform:translateX(-100%);
+        -moz-transform:translateX(-100%);
+        -o-transform:translateX(-100%);
+        transform:translateX(-100%);
+    }
+
+    40%
+    {
+        -webkit-transform:translateX(0%);
+        -moz-transform:translateX(0%);
+        -o-transform:translateX(0%);
+        transform:translateX(0%);
+    }
+
+    60%
+    {
+        -webkit-transform:translateX(0%);
+        -moz-transform:translateX(0%);
+        -o-transform:translateX(0%);
+        transform:translateX(0%);
+    }
+
+    100%
+    {
+        -webkit-transform:translateX(100%);
+        -moz-transform:translateX(100%);
+        -o-transform:translateX(100%);
+        transform:translateX(100%);
+    }
+
+}
+ at -moz-keyframes shift-rightwards
+{
+    0%
+    {
+        -webkit-transform:translateX(-100%);
+        -moz-transform:translateX(-100%);
+        -o-transform:translateX(-100%);
+        transform:translateX(-100%);
+    }
+
+    40%
+    {
+        -webkit-transform:translateX(0%);
+        -moz-transform:translateX(0%);
+        -o-transform:translateX(0%);
+        transform:translateX(0%);
+    }
+
+    60%
+    {
+        -webkit-transform:translateX(0%);
+        -moz-transform:translateX(0%);
+        -o-transform:translateX(0%);
+        transform:translateX(0%);
+    }
+
+    100%
+    {
+        -webkit-transform:translateX(100%);
+        -moz-transform:translateX(100%);
+        -o-transform:translateX(100%);
+        transform:translateX(100%);
+    }
+
+}
+ at -o-keyframes shift-rightwards
+{
+    0%
+    {
+        -webkit-transform:translateX(-100%);
+        -moz-transform:translateX(-100%);
+        -o-transform:translateX(-100%);
+        transform:translateX(-100%);
+    }
+
+    40%
+    {
+        -webkit-transform:translateX(0%);
+        -moz-transform:translateX(0%);
+        -o-transform:translateX(0%);
+        transform:translateX(0%);
+    }
+
+    60%
+    {
+        -webkit-transform:translateX(0%);
+        -moz-transform:translateX(0%);
+        -o-transform:translateX(0%);
+        transform:translateX(0%);
+    }
+
+    100%
+    {
+        -webkit-transform:translateX(100%);
+        -moz-transform:translateX(100%);
+        -o-transform:translateX(100%);
+        transform:translateX(100%);
+    }
+
+}
+ at keyframes shift-rightwards
+{
+    0%
+    {
+        -webkit-transform:translateX(-100%);
+        -moz-transform:translateX(-100%);
+        -o-transform:translateX(-100%);
+        transform:translateX(-100%);
+    }
+
+    40%
+    {
+        -webkit-transform:translateX(0%);
+        -moz-transform:translateX(0%);
+        -o-transform:translateX(0%);
+        transform:translateX(0%);
+    }
+
+    60%
+    {
+        -webkit-transform:translateX(0%);
+        -moz-transform:translateX(0%);
+        -o-transform:translateX(0%);
+        transform:translateX(0%);
+    }
+
+    100%
+    {
+        -webkit-transform:translateX(100%);
+        -moz-transform:translateX(100%);
+        -o-transform:translateX(100%);
+        transform:translateX(100%);
+    }
+}
+/* *********************** */
+/*        Crosshairs       */
+/* *********************** */
+.crosshairs {
+    left: calc(50% - 5px);
+    position: absolute;
+    top: calc(50% - 9px);
+    text-align: center;
+    z-index: 1000;
+}
+.crosshairs:before {
+    content: '✚';
+}
+/* *********************** */
+/*          Alert          */
+/* *********************** */
+.kosmtik-alert {
+    height: 0;
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    right: 60px;
+    transition: height 500ms ease-in;
+    background-color: #444;
+    color: #efefef;
+    z-index: 10000;
+    font-size: 12px;
+    display: none;
+}
+.kosmtik-alert .close {
+    display: block;
+    text-align: right;
+    color: #efefef;
+}
+.kosmtik-alert .close:before {
+    content: '❌ ';
+    font-family: 'dejavu_sansbook';
+}
+.alert .kosmtik-alert {
+    padding: 10px;
+    height: auto;
+    display: block;
+}
+
+/* *********************** */
+/*        Help panel       */
+/* *********************** */
+.help-panel .shortcuts {
+    width: 100%;
+    font-size: 0.8em;
+}
+.help-panel .shortcuts th {
+    text-align: left;
+}
+.help-panel .shortcuts td {
+    text-align: right;
+}
+/* *********************** */
+/*      Export panel       */
+/* *********************** */
+.extent-caption {
+    background-color: #444;
+    color: #efefef;
+    min-width: 100px;
+    padding: 5px;
+    display: none;
+    white-space: nowrap;
+}
+.extent-caption.show {
+    display: block;
+}
+
+
+/* *********************** */
+/*     Command palette     */
+/* *********************** */
+.k-command-palette .k-autcomplete-input {
+    display: inline-block;
+    height: 32px;
+    line-height: 32px;
+    margin-bottom: 0;
+    width: 400px;
+    padding: 4px;
+    background-color: #555;
+    color: #ecf0f1;
+    border: none;
+}
+ul.k-autocomplete-results {
+    background-color: #555;
+    box-shadow: 0 4px 9px #999999;
+    position: absolute;
+    z-index: 10000;
+}
+.k-autocomplete-results li {
+    line-height: 1.5em;
+    padding: 5px 10px;
+    overflow: hidden;
+    white-space: nowrap;
+    font-size: 0.9em;
+    color: #bcbcbc;
+}
+.k-autocomplete-results li strong {
+    display: block;
+    font-family: 'fira_sanslight';
+    font-weight: normal;
+    padding: 4px;
+}
+.k-autocomplete-results li.on {
+    background-color: #2c3e50;
+    cursor: pointer;
+    color: #ecf0f1;
+}
+.k-autocomplete-results li.k-autocomplete-no-result {
+    text-align: center;
+    color: #bbb;
+    font-size: 0.9em;
+    line-height: 40px;
+}
+.k-autocomplete-results li .shortcut {
+    float: right;
+    background-color: #333;
+    padding: 4px;
+}
+
+/* *********************** */
+/*   Data inspector popup  */
+/* *********************** */
+.leaflet-popup-content .data-inspector table {
+    width: 100%;
+}
+.leaflet-popup-content .data-inspector table + table {
+    border-top: 1px solid #666;
+}
+
+
+/* *********************** */
+/*   Override third party  */
+/* *********************** */
+.leaflet-control a, .leaflet-control a:hover {
+    background-color: #444;
+    color: #ecf0f1;
+    border: none;
+    border-radius: 0!important;
+    text-decoration: none;
+    text-align: center;
+    display: block;
+}
+.leaflet-control a:hover {
+    background-color: #555;
+}
+.leaflet-bar a.leaflet-disabled {
+    background-color: #777;
+}
+.leaflet-control {
+    border: 1px solid #222;
+    border-radius: 2px;
+    box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65);
+    width: 54px;
+}
+.leaflet-control-zoom-in, .leaflet-control-zoom-out {
+    float: left;
+}
+.leaflet-control-zoom-in {
+    border-right: 1px solid #222;
+}
+.leaflet-control-scale {
+    width: auto;
+    border: none;
+}
+.leaflet-popup-content-wrapper, .leaflet-popup-tip {
+    background: none repeat scroll 0 0 #444;
+    box-shadow: 0 3px 7px rgba(0, 0, 0, 0.4);
+    color: #efefef;
+}
+.leaflet-popup-content-wrapper {
+    border-radius: 4px;
+}
diff --git a/src/front/Core.js b/src/front/Core.js
new file mode 100644
index 0000000..010e5d0
--- /dev/null
+++ b/src/front/Core.js
@@ -0,0 +1,279 @@
+L.Kosmtik = L.K = {};
+
+
+/*************/
+/*   Utils   */
+/*************/
+L.Kosmtik.buildQueryString = function (params) {
+    var queryString = [];
+    for (var key in params) {
+        queryString.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key]));
+    }
+    return queryString.join('&');
+};
+
+L.Kosmtik.Xhr = {
+
+    _ajax: function (settings) {
+        var xhr = new window.XMLHttpRequest();
+        xhr.open(settings.verb, settings.uri, true);
+        xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
+        xhr.onreadystatechange = function() {
+            if (xhr.readyState === 4) {
+                settings.callback.call(settings.context || xhr, xhr.status, xhr.responseText, xhr);
+            }
+        };
+        xhr.send(settings.data);
+        return xhr;
+    },
+
+    get: function(uri, options) {
+        options.verb = 'GET';
+        options.uri = uri;
+        return L.K.Xhr._ajax(options);
+    },
+
+    post: function(uri, options) {
+        options.verb = 'POST';
+        options.uri = uri;
+        return L.K.Xhr._ajax(options);
+    }
+
+};
+
+L.Kosmtik.Poll = L.Evented.extend({
+
+    initialize: function (uri) {
+        this.uri = uri;
+        this.delay = 1;
+    },
+
+    poll: function () {
+        L.K.Xhr.get(this.uri, {
+            callback: this.polled,
+            context: this
+        });
+    },
+
+    polled: function (status, data) {
+        if (status === 204 || status === 200) this.fire('polled');
+        if (status === 204) return this.loop(1);
+        if (status !== 200 || !data) return this.onError({status: status, error: data});
+        try {
+            data = JSON.parse(data);
+        } catch (err) {
+            return this.onError({error: err});
+        }
+        for (var i = 0; i < data.length; i++) {
+            this.fire('message', data[i]);
+        }
+        this.loop(1);
+    },
+
+    onError: function (e) {
+        this.fire('error', e);
+        this.loop(++this.delay);
+    },
+
+    loop: function (delay) {
+        this.delay = delay;
+        this._id = window.setTimeout(L.bind(this.poll, this), this.delay * 1000);
+    },
+
+    start: function () {
+        if (!this._id) this.loop(1);
+        this.fire('start');
+        return this;
+    },
+
+    stop: function () {
+        if (this._id) {
+            window.clearTimeout(this._id);
+            this._id = null;
+        }
+        this.fire('stop');
+        return this;
+    }
+
+});
+
+L.Kosmtik.Switch = L.FormBuilder.CheckBox.extend({
+
+    build: function () {
+        L.FormBuilder.CheckBox.prototype.build.apply(this);
+        this.input.parentNode.appendChild(this.label);
+        L.DomUtil.addClass(this.input.parentNode, 'with-switch');
+        var id = (this.builder.options.id || Date.now()) + '.' + this.name;
+        this.label.setAttribute('for', id);
+        L.DomUtil.addClass(this.input, 'switch');
+        this.input.id = id;
+    }
+
+});
+
+L.Kosmtik.Util = {};
+
+L.Kosmtik.Util.renderPropertiesTable = function (properties) {
+    var renderRow = function (container, key, value) {
+        if (!key || value === undefined) return;
+        var tr = L.DomUtil.create('tr', '', container);
+        L.DomUtil.create('th', '', tr).innerHTML = key;
+        L.DomUtil.create('td', '', tr).innerHTML = value;
+    };
+    var table = L.DomUtil.create('table');
+
+    for (var key in properties) {
+        renderRow(table, key, properties[key]);
+    }
+    return table;
+};
+
+L.K.Crosshairs = L.Layer.extend({
+
+    initialize: function (map) {
+        this.icon = L.DomUtil.create('div', 'crosshairs', map._container);
+        map.settingsForm.addElement(['showCrosshairs', {handler: L.K.Switch, label: 'Show crosshairs in the center of the map'}]);
+        map.on('settings:synced', this.toggle, this);
+        this.toggle();
+    },
+
+    addTo: function (map) {
+        map.addLayer(this);
+        return this;
+    },
+
+    onAdd: function (map) {
+        this.show();
+    },
+
+    onRemove: function (map) {
+        this.hide();
+    },
+
+    show: function () {
+        L.DomUtil.setOpacity(this.icon, 0.8);
+    },
+
+    hide: function () {
+        L.DomUtil.setOpacity(this.icon, 0);
+    },
+
+    toggle: function () {
+        if (L.K.Config.showCrosshairs) this.show();
+        else this.hide();
+    }
+
+});
+
+L.Kosmtik.Alert = L.Class.extend({
+
+    initialize: function (map, options) {
+        this._map = map;
+        L.setOptions(this, options);
+        this.container = L.DomUtil.create('div', 'kosmtik-alert', document.body);
+        this.closeButton = L.DomUtil.create('a', 'close', this.container);
+        this.content = L.DomUtil.create('div', 'content', this.container);
+        this.closeButton.href = '#';
+        this.closeButton.innerHTML = 'Close';
+        L.DomEvent
+            .on(this.closeButton, 'click', L.DomEvent.stop)
+            .on(this.closeButton, 'click', this.hide, this);
+        this._map.on('reload', this.hide, this);
+    },
+
+    show: function (options) {
+        this.content.innerHTML = options.content;
+        this._map.setState('alert');
+    },
+
+    hide: function () {
+        this._map.unsetState('alert');
+    }
+
+});
+
+L.K.Keys = {
+    LEFT: 37,
+    UP: 38,
+    RIGHT: 39,
+    DOWN: 40,
+    TAB: 9,
+    ENTER: 13,
+    ESC: 27,
+    APPLE: 91,
+    SHIFT: 16,
+    ALT: 17,
+    CTRL: 18,
+    A: 65,
+    B: 66,
+    C: 67,
+    D: 68,
+    E: 69,
+    F: 70,
+    G: 71,
+    H: 72,
+    I: 73,
+    J: 74,
+    K: 75,
+    L: 76,
+    M: 77,
+    N: 78,
+    O: 79,
+    P: 80,
+    Q: 81,
+    R: 82,
+    S: 83,
+    T: 84,
+    U: 85,
+    V: 86,
+    W: 87,
+    X: 88,
+    Y: 89,
+    Z: 90
+};
+L.K.KeysLabel = {};
+for (var k in L.K.Keys) L.K.KeysLabel[L.K.Keys[k]] = k;
+
+L.Kosmtik.Help = L.Class.extend({
+
+    initialize: function (map) {
+        this.map = map;
+        this.buildSidebar();
+    },
+
+    buildSidebar: function () {
+        var container = L.DomUtil.create('div', 'help-panel'),
+            title = L.DomUtil.create('h3', '', container);
+        title.innerHTML = 'Help';
+        this.buildShortcuts(container);
+        this.map.sidebar.addTab({
+            label: 'Help',
+            className: 'help',
+            content: container
+        });
+        this.map.sidebar.rebuild();
+        this.map.commands.add({
+            callback: this.openSidebar,
+            context: this,
+            name: 'Help: open'
+        });
+    },
+
+    openSidebar: function () {
+        this.map.sidebar.open('.help');
+    },
+
+    buildShortcuts: function (container) {
+        var title = L.DomUtil.create('h4', '', container),
+            shortcuts = L.DomUtil.create('table', 'shortcuts', container);
+        title.innerHTML = 'Keyboard shortcuts';
+        this.map.commands.each(function (specs) {
+            if (!specs.name || !specs.keyCode) return;
+            var row = L.DomUtil.create('tr', '', shortcuts);
+            if (specs.description) row.title = specs.description;
+            L.DomUtil.create('th', '', row).innerHTML = L.K.Command.makeLabel(specs);
+            L.DomUtil.create('td', '', row).innerHTML = specs.name;
+        }, this);
+    }
+
+});
diff --git a/src/front/DataInspector.js b/src/front/DataInspector.js
new file mode 100644
index 0000000..a64da3e
--- /dev/null
+++ b/src/front/DataInspector.js
@@ -0,0 +1,161 @@
+L.TileLayer.XRay = L.TileLayer.extend({
+
+    getTileUrl: function (tilePoint) {
+        this.options.version = Date.now();
+        this.options.showLayer = L.TileLayer.XRay.computeLayers();
+        this.options.background = L.K.Config.dataInspectorBackground || '';
+        return L.TileLayer.prototype.getTileUrl.call(this, tilePoint);
+    }
+
+});
+
+L.extend(L.TileLayer.XRay, {
+
+    // display only the checked layers
+    computeLayers: function () {
+        var showLayers = [];
+        for (var k in L.K.Config.dataInspectorLayers) {
+            if (L.K.Config.dataInspectorLayers[k] === true && k !== '__all__') showLayers.push(k);
+        }
+        return showLayers.join(',');
+    }
+
+});
+
+L.Kosmtik.DataInspector = L.Evented.extend({
+
+    initialize: function (map) {
+        this.map = map;
+        var options = {
+            minZoom: this.map.options.minZoom,
+            maxZoom: this.map.options.maxZoom
+        };
+        this.tilelayer = new L.TileLayer.XRay('./tile/{z}/{x}/{y}.xray?t={version}&layer={showLayer}&background={background}', options);
+        this.tilelayer.on('loading', function () { this.setState('loading'); }, this.map);
+        this.tilelayer.on('load', function () { this.unsetState('loading'); }, this.map);
+        this.createSidebarPanel();
+        this.createToolbarButton();
+        this.addCommands();
+        this.map.on('click', function (e) {
+            if (!L.K.Config.dataInspector) return;
+            var url = L.Util.template('./query/{z}/{lat}/{lng}/?layer={showLayers}', {
+                z: this.map.getZoom(),
+                lat: e.latlng.lat,
+                lng: e.latlng.lng,
+                showLayers: L.TileLayer.XRay.computeLayers()
+            });
+            L.K.Xhr.get(url, {
+                callback: function (status, data) {
+                    if (status !== 200) return;  // display message?
+                    data = JSON.parse(data);
+                    if (!data.length) return;
+                    var content = L.DomUtil.create('div', 'data-inspector');
+                    data.map(function (feature) {
+                        feature.attributes.layer = feature.layer;
+                        content.appendChild(L.K.Util.renderPropertiesTable(feature.attributes));
+                    });
+                    this.map.openPopup(content, e.latlng, {autoPan: false});
+                },
+                context: this
+            });
+        }, this);
+        this.map.on('reload', this.redraw, this);
+    },
+
+    createSidebarPanel: function () {
+        this.container = L.DomUtil.create('div', 'data-inspector-form');
+        this.title = L.DomUtil.create('h3', '', this.container);
+        this.formContainer = L.DomUtil.create('div', '', this.container);
+        this.title.innerHTML = 'Data Inspector';
+        var layers = L.K.Config.project.layers.map(function (l) { return l.name; });
+        var backgrounds = [['black', 'black'], ['transparent', 'transparent']];
+
+        var layerSettings = [['dataInspectorLayers.__all__', {handler: L.FormBuilder.LabeledCheckBox, label: 'Show All' } ]];
+        layerSettings = layers.reduce(function (prev, curr) {
+            prev.push(['dataInspectorLayers.' + curr, {handler: L.FormBuilder.LabeledCheckBox, label: 'Show "' + curr + '"'}]);
+            return prev;
+        }, layerSettings);
+        this.sidebarForm = new L.K.FormBuilder(L.K.Config, [
+            ['dataInspector', {handler: L.K.Switch, label: 'Active'}],
+            ['dataInspectorBackground', {handler: L.FormBuilder.Select, helpText: 'Choose inspector background', selectOptions: backgrounds}]
+        ].concat(layerSettings));
+        this.formContainer.appendChild(this.sidebarForm.build());
+        this.sidebarForm.on('postsync', function (e) {
+            if (e.helper.field === 'dataInspector') this.toggle();
+            else if (e.helper.field === 'dataInspectorBackground') this.redraw();
+            else if (e.helper.field.indexOf('dataInspectorLayers') === 0) {
+                if (e.helper.field !== 'dataInspectorLayers.__all__') L.K.Config.dataInspectorLayers.__all__ = false;
+                else for (var k in L.K.Config.dataInspectorLayers) L.K.Config.dataInspectorLayers[k] = k === '__all__';
+                this.sidebarForm.fetchAll();
+                this.redraw();
+            }
+        }, this);
+        this.map.sidebar.addTab({
+            label: 'Inspect',
+            className: 'data-inspector',
+            content: this.container,
+            callback: this.sidebarForm.build,
+            context: this.sidebarForm
+        });
+        this.map.sidebar.rebuild();
+    },
+
+    openSidebar: function () {
+        this.map.sidebar.open('.data-inspector');
+    },
+
+    createToolbarButton: function () {
+        var button = L.DomUtil.create('li', 'autoreload with-switch');
+        this.toolbarForm = new L.K.FormBuilder(L.K.Config, [
+            ['dataInspector', {handler: L.K.Switch, label: 'Data Inspector'}]
+        ]);
+        button.appendChild(this.toolbarForm.build());
+        this.toolbarForm.on('postsync', this.toggle, this);
+        this.map.toolbar.addTool(button);
+    },
+
+    addCommands: function () {
+        var toggleCallback = function () {
+            L.K.Config.dataInspector = !L.K.Config.dataInspector;
+            this.toggle();
+        };
+        this.map.commands.add({
+            keyCode: L.K.Keys.I,
+            shiftKey: true,
+            ctrlKey: true,
+            callback: toggleCallback,
+            context: this,
+            name: 'Data inspector: toggle layer'
+        });
+        this.map.commands.add({
+            callback: this.openSidebar,
+            context: this,
+            name: 'Data inspector: configure'
+        });
+    },
+
+    toggle: function () {
+        this.toolbarForm.fetchAll();
+        this.sidebarForm.fetchAll();
+        if (L.K.Config.dataInspector) this.tilelayer.addTo(this.map);
+        else this.map.removeLayer(this.tilelayer);
+        this.map.closePopup();
+    },
+
+    redraw: function () {
+        this.tilelayer.redraw();
+    }
+
+});
+
+L.FormBuilder.LabeledCheckBox = L.FormBuilder.CheckBox.extend({
+
+    build: function () {
+        L.FormBuilder.CheckBox.prototype.build.call(this);
+        this.label = L.DomUtil.create('label', '', this.input.parentNode);
+        this.label.innerHTML = this.options.label;
+    },
+
+    buildLabel: function () {/* We take control over label. */}
+
+});
diff --git a/src/front/FormBuilder.js b/src/front/FormBuilder.js
new file mode 100644
index 0000000..45475e4
--- /dev/null
+++ b/src/front/FormBuilder.js
@@ -0,0 +1,8 @@
+L.Kosmtik.FormBuilder = L.FormBuilder.extend({
+
+    defaultOptions: {
+        width: {handler: 'IntInput', placeholder: 'Width', helpText: 'Choose the width'},
+        height: {handler: 'IntInput', placeholder: 'Height', helpText: 'Choose the height'}
+    }
+
+});
diff --git a/src/front/Map.js b/src/front/Map.js
new file mode 100644
index 0000000..f82ca03
--- /dev/null
+++ b/src/front/Map.js
@@ -0,0 +1,182 @@
+L.Kosmtik.Map = L.Map.extend({
+
+    options: {
+        attributionControl: false
+    },
+
+    initialize: function (options) {
+        this.sidebar = new L.Kosmtik.Sidebar().addTo(this);
+        this.toolbar = new L.Kosmtik.Toolbar().addTo(this);
+        this.commands = new L.Kosmtik.Command(this);
+        this.settingsForm = new L.K.SettingsForm(this);
+        this.settingsForm.addElement(['autoReload', {handler: L.K.Switch, label: 'Autoreload', helpText: 'Reload map as soon as a project file is changed on the server.'}]);
+        this.settingsForm.addElement(['backendPolling', {handler: L.K.Switch, label: '(Advanced) Poll backend for project updates'}]);
+        this.createPollIndicator();
+        this.createReloadButton();
+        this.dataInspector = new L.K.DataInspector(this);
+        L.Map.prototype.initialize.call(this, 'map', options);
+        this.loader = L.DomUtil.create('div', 'map-loader', this._controlContainer);
+        this.crosshairs = new L.K.Crosshairs(this);
+        this.alert = new L.K.Alert(this);
+        this.metatilesBounds = new L.K.MetatileBounds(this);
+        var tilelayerOptions = {
+            version: L.K.Config.project.loadTime,
+            minZoom: this.options.minZoom,
+            maxZoom: this.options.maxZoom
+        };
+        this.tilelayer = new L.TileLayer('./tile/{z}/{x}/{y}.png?t={version}', tilelayerOptions).addTo(this);
+        this.tilelayer.on('loading', function () {
+            this.setState('loading');
+        }, this);
+        this.tilelayer.on('load', function () {
+            this.unsetState('loading');
+        }, this);
+        L.control.scale().addTo(this);
+        this.initPoller();
+        this.on('dirty:on', function () {
+            if (L.K.Config.autoReload) this.reload();
+        });
+        this.on('settings:synced', function (e) {
+            if (e.helper.field === 'backendPolling') this.togglePoll();
+        });
+        this.help = new L.Kosmtik.Help(this);
+        if(L.K.Config.project.name.length) document.title = L.K.Config.project.name + ' — Kosmtik';
+    },
+
+    setState: function (state) {
+        if (!L.DomUtil.hasClass(document.body, state)) {
+            L.DomUtil.addClass(document.body, state);
+            this.fire(state + ':on');
+        }
+    },
+
+    unsetState: function (state) {
+        if (L.DomUtil.hasClass(document.body, state)) {
+            L.DomUtil.removeClass(document.body, state);
+            this.fire(state + ':off');
+        }
+    },
+
+    checkState: function (state) {
+        return L.DomUtil.hasClass(document.body, state);
+    },
+
+    reload: function () {
+        this.unsetState('dirty');
+        this.setState('loading');
+        this.fire('reload');
+        L.K.Xhr.post('./reload/', {
+            callback: function (status, data) {
+                if (status === 200 && data) {
+                    L.K.Config.project = JSON.parse(data);
+                    this.tilelayer.options.version = L.K.Config.project.loadTime;
+                    this.tilelayer.redraw();
+                    this.fire('reloaded');
+                }
+                this.unsetState('loading');
+            },
+            context: this
+        });
+    },
+
+    createReloadButton: function () {
+        var reload = L.DomUtil.create('li', 'reload');
+        reload.innerHTML = 'Reload';
+        L.DomEvent.on(reload, 'click', function () {
+            this.reload();
+        }, this);
+        this.toolbar.addTool(reload);
+        this.commands.add({
+            keyCode: L.K.Keys.R,
+            shiftKey: true,
+            ctrlKey: true,
+            callback: this.reload,
+            context: this,
+            name: 'Map: reload'
+        });
+        this.commands.add({
+            keyCode: L.K.Keys.A,
+            shiftKey: true,
+            ctrlKey: true,
+            altKey: true,
+            callback: function () { this.settingsForm.toggle('autoReload'); },
+            context: this,
+            name: 'Autoreload: toggle',
+            description: 'Autoreload or not when project has changed'
+        });
+    },
+
+    createPollIndicator: function () {
+        var button = L.DomUtil.create('li', 'poll-indicator');
+        button.innerHTML = '⇵';
+        button.title = 'Sync status';
+        this.toolbar.addTool(button);
+    },
+
+    initPoller: function () {
+        this.poll = new L.K.Poll('./poll/');
+        this.poll.on('message', function (e) {
+            if (e.isDirty) this.setState('dirty');
+            if (e.error) this.alert.show({content: e.error, level: 'error'});
+        }, this);
+        this.poll.on('error', function () {
+            this.setState('polling-error');
+        }, this);
+        this.poll.on('polled', function () {
+            this.unsetState('polling-error');
+        }, this);
+        this.poll.on('start', function () {
+            this.setState('polling');
+        }, this);
+        this.poll.on('stop', function () {
+            this.unsetState('polling');
+        }, this);
+        this.togglePoll();
+        var commandCallback = function () {
+            this.settingsForm.toggle('backendPolling');
+            this.togglePoll();
+        };
+        this.commands.add({
+            keyCode: L.K.Keys.P,
+            shiftKey: true,
+            ctrlKey: true,
+            altKey: true,
+            callback: commandCallback,
+            context: this,
+            name: 'Poller: toggle'
+        });
+    },
+
+    togglePoll: function () {
+        if (L.K.Config.backendPolling) this.poll.start();
+        else this.poll.stop();
+    }
+
+});
+
+L.Kosmtik.ZoomIndicator = L.Control.extend({
+
+    options: {
+        position: 'topleft'
+    },
+
+    onAdd: function (map) {
+        this.map = map;
+        this.container = L.DomUtil.create('div', 'zoom-indicator');
+        map.on('zoomend', this.update, this);
+        this.update();
+        return this.container;
+    },
+
+    update: function () {
+        this.container.textContent = this.map.getZoom();
+    }
+
+});
+
+
+L.K.Map.addInitHook(function () {
+    this.whenReady(function () {
+        (new L.K.ZoomIndicator()).addTo(this);
+    });
+});
diff --git a/src/front/MetatilesBounds.js b/src/front/MetatilesBounds.js
new file mode 100644
index 0000000..d6e0f3c
--- /dev/null
+++ b/src/front/MetatilesBounds.js
@@ -0,0 +1,92 @@
+L.Kosmtik.MetatileBounds = L.TileLayer.extend({
+
+    initialize: function (map) {
+        this.map = map;
+        this.map.settingsForm.addElement(['showMetatiles', {handler: L.K.Switch, label: 'Display metatiles bounds (ctrl-alt-M)'}]);
+        this.map.on('settings:synced', function (e) {
+            if (e.helper.field === 'showMetatiles') this.toggle();
+        }, this);
+        this.map.commands.add({
+            keyCode: L.K.Keys.M,
+            altKey: true,
+            ctrlKey: true,
+            callback: function () { this.map.settingsForm.toggle('showMetatiles'); },
+            context: this,
+            name: 'Metatiles bounds: toggle view'
+        });
+        L.TileLayer.prototype.initialize.call(this, '');
+        this.setTileSize();
+    },
+
+    toggle: function () {
+        if (L.K.Config.showMetatiles) this.map.addLayer(this);
+        else this.map.removeLayer(this);
+    },
+
+    resetVectorLayer: function () {
+        if (this.vectorlayer) this.vectorlayer.clearLayers();
+    },
+
+    removeVectorLayer: function () {
+        this._map.removeLayer(this.vectorlayer);
+    },
+
+    onAdd: function (map) {
+        this._map = map;
+        this.vectorlayer = new L.FeatureGroup();
+        map.addLayer(this.vectorlayer);
+        // Delete the clusters to prevent from having several times
+        // the same data
+        map.on('zoomstart', this.resetVectorLayer, this);
+        map.on('reloaded', this.reset, this);
+        L.TileLayer.prototype.onAdd.call(this, map);
+    },
+
+    onRemove: function (map) {
+        map.off('zoomstart', this.resetVectorLayer, this);
+        map.off('reloaded', this.reset, this);
+        this.removeVectorLayer();
+        L.TileLayer.prototype.onRemove.call(this, map);
+    },
+
+    _addTile: function (tilePoint, container) {
+        L.TileLayer.prototype._addTile.call(this, tilePoint, container);
+        this.addData(tilePoint);
+    },
+
+    addData: function (tilePoint) {
+        var tileSize = this.options.tileSize,
+            nwPoint = tilePoint.multiplyBy(tileSize),
+            sw = this._map.unproject(nwPoint.add([0, tileSize])),
+            se = this._map.unproject(nwPoint.add([tileSize, tileSize])),
+            ne = this._map.unproject(nwPoint.add([tileSize, 0]));
+        var options = {
+            color: '#444',
+            weight: 1,
+            opacity: 0.7,
+            fill: false,
+            clickable: false,
+            noClip: true
+        };
+        this.vectorlayer.addLayer(L.polyline([sw, se, ne], options));
+        options.color = '#fff';
+        options.dashArray = '10,10';
+        options.opacity = 0.8;
+        this.vectorlayer.addLayer(L.polyline([sw, se, ne], options));
+    },
+
+    setTileSize: function () {
+        this.options.tileSize = L.K.Config.project.metatile * 256;
+    },
+
+    redraw: function () {
+        if (this.vectorlayer) this.vectorlayer.clearLayers();
+        L.TileLayer.prototype.redraw.call(this);
+    },
+
+    reset: function () {
+        this.setTileSize();
+        this.redraw();
+    }
+
+});
diff --git a/src/front/Settings.js b/src/front/Settings.js
new file mode 100644
index 0000000..d7b952b
--- /dev/null
+++ b/src/front/Settings.js
@@ -0,0 +1,60 @@
+L.Kosmtik.SettingsForm = L.Class.extend({
+
+    initialize: function (map) {
+        this.container = L.DomUtil.create('div', 'settings-form');
+        this.title = L.DomUtil.create('h3', '', this.container);
+        this.formContainer = L.DomUtil.create('div', '', this.container);
+        this.title.innerHTML = 'UI Settings';
+        this.elements = [];
+        this.map = map;
+        this.builder = new L.K.FormBuilder(L.K.Config, this.elements);
+        this.formContainer.appendChild(this.builder.build());
+        this.builder.on('postsync', function (e) {
+            this.fire('settings:synced', e);
+        }, this.map);
+        this.map.sidebar.addTab({
+            label: 'Settings',
+            className: 'settings',
+            content: this.container,
+            callback: this.build,
+            context: this
+        });
+        this.map.sidebar.rebuild();
+        this.map.commands.add({
+            callback: this.open,
+            context: this,
+            name: 'Settings: configure'
+        });
+    },
+
+    build: function () {
+        if (this.elements.length) {
+            this.builder.setFields(this.elements);
+            this.builder.build();
+        }
+    },
+
+    addElement: function (element) {
+        this.elements.push(element);
+        this.build();
+    },
+
+    fetchAll: function () {
+        this.builder.fetchAll();
+    },
+
+    set: function (key, value) {
+        L.K.Config[key] = value;
+        this.builder.helpers[key].fetch();
+        this.builder.helpers[key].sync();
+    },
+
+    toggle: function (key) {
+        this.set(key, !L.K.Config[key]);
+    },
+
+    open: function () {
+        this.map.sidebar.open('.settings');
+    }
+
+});
diff --git a/src/front/Sidebar.css b/src/front/Sidebar.css
new file mode 100644
index 0000000..74de1de
--- /dev/null
+++ b/src/front/Sidebar.css
@@ -0,0 +1,87 @@
+.sidebar {
+  position: absolute;
+  right: 0;
+  top: 40px;
+  bottom: 0;
+  width: 100%;
+  overflow: hidden;
+  z-index: 2000; 
+  width: 460px;
+  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65);
+  /*transition: width 500ms;  Creates problem on FF when export extent is displayed */
+  background-color: #444;
+  color: #efefef;
+}
+.sidebar.large {
+  width: calc(100% - 80px);
+}
+.sidebar.collapsed {
+  width: 60px; 
+}
+.sidebar-tabs {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  right: 0;
+  width: 60px;
+  height: 100%;
+  margin: 0;
+  padding: 0;
+  text-align: center;
+}
+.sidebar-tabs > li {
+  width: 100%;
+  height: 60px;
+  font-size: 12px;
+  overflow: hidden;
+  transition: all 80ms;
+  color: #efefef;
+  line-height: 60px;
+  cursor: pointer;
+  border-bottom: 1px solid #555;
+}
+.sidebar-tabs > li:hover {
+  background-color: #555;
+}
+.sidebar-tabs > li.active {
+  color: #fff;
+  background-color: #666;
+}
+
+.sidebar-content {
+  position: absolute;
+  right: 60px;
+  top: 0;
+  bottom: 0;
+  background-color: #666;
+  overflow-x: hidden;
+  overflow-y: auto;
+  padding-left: 10px;
+}
+
+.sidebar-pane {
+  display: none;
+  right: 0;
+  padding: 10px 20px;
+  width: 100%;
+  min-width: 400px;
+}
+.sidebar-pane.active {
+  display: block;
+}
+.sidebar-pane h3 {
+  margin-bottom: 28px;
+}
+.sidebar-map .leaflet-right {
+  transition: right 500ms;
+  right: 470px;
+}
+.sidebar.collapsed ~ .sidebar-map .leaflet-right {
+  right: 70px;
+}
+.layer-label {
+  padding-top: 4px;
+}
+.layer-name {
+  margin-left: 10px;
+}
diff --git a/src/front/Sidebar.js b/src/front/Sidebar.js
new file mode 100644
index 0000000..fa02e7a
--- /dev/null
+++ b/src/front/Sidebar.js
@@ -0,0 +1,104 @@
+L.Kosmtik.Sidebar = L.Control.extend({
+    includes: L.Mixin.Events,
+
+    initialize: function (options) {
+
+        L.setOptions(this, options);
+
+        this._sidebar = L.DomUtil.create('div', 'sidebar collapsed');
+        document.body.insertBefore(this._sidebar, document.body.firstChild);
+        this._container = L.DomUtil.create('ul', 'sidebar-content', this._sidebar);
+        this._tabs = L.DomUtil.create('ul', 'sidebar-tabs', this._sidebar);
+
+        this._tabitems = [];
+        this._panes = [];
+    },
+
+    addTab: function (options) {
+        options = options || {};
+        var tab = L.DomUtil.create('li', options.className || '', this._tabs);
+        tab.innerHTML = options.label;
+        tab._sidebar = this;
+        if (options.callback) {
+            this.on('open', function (e) {
+                if (e.el === tab) options.callback.call(options.context || this);
+            });
+        }
+        this.on('opening', function (e) {
+            if (e.el !== tab) return;
+            if (options.large) L.DomUtil.addClass(this._sidebar, 'large');
+            else L.DomUtil.removeClass(this._sidebar, 'large');
+        });
+        var pane = L.DomUtil.create('li', 'sidebar-pane ' + (options.className || ''), this._container);
+        if (options.content.nodeType && options.content.nodeType === 1) {
+            pane.appendChild(options.content);
+        }
+        else {
+            pane.innerHTML = options.content;
+        }
+        tab._pane = pane;
+        this._tabitems.push(tab);
+        this._panes.push(pane);
+    },
+
+    addTo: function (map) {
+        this._map = map;
+        L.DomEvent.on(document, 'keyup', this._onKeyUp, this);
+        for (var i = this._tabitems.length - 1; i >= 0; i--) {
+            L.DomEvent.on(this._tabitems[i], 'click', this._onClick, this);
+        }
+        return this;
+    },
+
+    removeFrom: function (map) {
+        this._map = null;
+        L.DomEvent.off(document, 'keyup', this._onKeyUp, this);
+        for (var i = this._tabitems.length - 1; i >= 0; i--) {
+            L.DomEvent.off(this._tabitems[i], 'click', this._onClick, this);
+        }
+
+        return this;
+    },
+
+    rebuild: function () {
+        var map = this._map;
+        this.removeFrom(map).addTo(map);
+    },
+
+    closeAll: function () {
+        for (var i = this._panes.length - 1; i >= 0; i--) L.DomUtil.removeClass(this._panes[i], 'active');
+        for (var j = this._tabitems.length - 1; j >= 0; j--) L.DomUtil.removeClass(this._tabitems[j], 'active');
+    },
+
+    open: function (el) {
+        this.closeAll();
+        if (typeof el === 'string') el = this._tabs.querySelector(el);
+        if (!el) return;
+        this.fire('opening', {el: el});
+        L.DomUtil.addClass(el, 'active');
+        L.DomUtil.addClass(el._pane, 'active');
+        L.DomUtil.removeClass(this._sidebar, 'collapsed');
+        this.fire('open', {el: el});
+    },
+
+    close: function () {
+        if (!L.DomUtil.hasClass(this._sidebar, 'collapsed')) {
+            this.closeAll();
+            this.fire('closing');
+            L.DomUtil.addClass(this._sidebar, 'collapsed');
+            this._map.invalidateSize();
+        }
+    },
+
+    _onClick: function(e) {
+        this.fire('tab:click', {el: e.target});
+        if (L.DomUtil.hasClass(e.target, 'active')) this.close();
+        else this.open(e.target);
+    },
+
+    _onKeyUp: function (e) {
+        if (e.keyCode === L.K.Keys.ESC) {
+            this.close();
+        }
+    }
+});
diff --git a/src/front/Toolbar.css b/src/front/Toolbar.css
new file mode 100644
index 0000000..152c232
--- /dev/null
+++ b/src/front/Toolbar.css
@@ -0,0 +1,87 @@
+.toolbar {
+    z-index: 1001;
+    box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65);
+    background-color: #444;
+    color: #efefef;
+    height: 40px;
+    line-height: 40px;
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+}
+.toolbar a.brand {
+    font-size: 1.2em;
+    display: inline-block;
+    font-weight: normal;
+    padding-left: 45px;
+    background-image: url('./header_logo.svg');
+    background-repeat: no-repeat;
+    background-size: 30px;
+    background-position: 5px center;
+    color: #efefef;
+    text-decoration: none;
+}
+.toolbar ul {
+    display: inline-block;
+    text-align: right;
+    float: right;
+    margin-right: 60px;
+    border-right: 1px solid #555;
+}
+.toolbar ul li {
+    display: inline-block;
+    padding: 0 10px;
+    border-left: 1px solid #555;
+    cursor: pointer;
+    vertical-align: middle;
+    height: 100%;
+    font-size: 12px;
+    float: right;
+}
+.toolbar ul li:hover {
+  background-color: #555;
+}
+.dirty li.reload:hover,
+.dirty li.reload {
+    background-color: #d35400;
+}
+.dirty li.reload:hover {
+    background-color: #e67e22;
+}
+li.reload:before {
+    content: '⟳ ';
+    font-family: 'dejavu_sansbook';
+}
+.dirty li.reload:after {
+    content: ' ⚡';
+    font-family: 'dejavu_sansbook';
+}
+.toolbar ul li.with-switch {
+    height: 40px;
+    line-height: 40px;
+    padding-right: 20px;
+    padding-top: 7px;
+}
+.toolbar ul li.with-switch .formbox {
+    border: none;
+    background-color: transparent;
+}
+.toolbar ul li.with-switch label {
+    text-indent: 1em;
+}
+.toolbar .poll-indicator {
+    font-size: 16px;
+    cursor: default;
+    text-decoration: line-through;
+    font-family: 'dejavu_sansbook';
+}
+.toolbar .poll-indicator:hover {
+    background-color: #444;
+}
+.polling .toolbar .poll-indicator {
+    text-decoration: none;
+}
+.polling-error .toolbar .poll-indicator {
+    background-color: #d35400;
+}
diff --git a/src/front/Toolbar.js b/src/front/Toolbar.js
new file mode 100644
index 0000000..e84333f
--- /dev/null
+++ b/src/front/Toolbar.js
@@ -0,0 +1,30 @@
+L.Kosmtik.Toolbar = L.Control.extend({
+    includes: L.Mixin.Events,
+
+    initialize: function (options) {
+
+        L.setOptions(this, options);
+
+        this.container = L.DomUtil.create('div', 'toolbar');
+        document.body.insertBefore(this.container, document.body.firstChild);
+        var a = L.DomUtil.create('a', 'brand', this.container);
+        a.innerHTML = 'kosmtik';
+        a.href = '/';
+        this.toolsContainer = L.DomUtil.create('ul', 'tools', this.container);
+    },
+
+    addTo: function (map) {
+        this._map = map;
+        return this;
+    },
+
+    removeFrom: function (map) {
+        this._map = null;
+        return this;
+    },
+
+    addTool: function (tool) {
+        this.toolsContainer.appendChild(tool);
+    }
+
+});
diff --git a/src/front/fonts/DejaVuSans-webfont.eot b/src/front/fonts/DejaVuSans-webfont.eot
new file mode 100644
index 0000000..5460a09
Binary files /dev/null and b/src/front/fonts/DejaVuSans-webfont.eot differ
diff --git a/src/front/fonts/DejaVuSans-webfont.ttf b/src/front/fonts/DejaVuSans-webfont.ttf
new file mode 100644
index 0000000..ff0430e
Binary files /dev/null and b/src/front/fonts/DejaVuSans-webfont.ttf differ
diff --git a/src/front/fonts/DejaVuSans-webfont.woff b/src/front/fonts/DejaVuSans-webfont.woff
new file mode 100644
index 0000000..feeeb2a
Binary files /dev/null and b/src/front/fonts/DejaVuSans-webfont.woff differ
diff --git a/src/front/fonts/FiraSans-Bold.eot b/src/front/fonts/FiraSans-Bold.eot
new file mode 100644
index 0000000..07323b6
Binary files /dev/null and b/src/front/fonts/FiraSans-Bold.eot differ
diff --git a/src/front/fonts/FiraSans-Bold.ttf b/src/front/fonts/FiraSans-Bold.ttf
new file mode 100644
index 0000000..093503b
Binary files /dev/null and b/src/front/fonts/FiraSans-Bold.ttf differ
diff --git a/src/front/fonts/FiraSans-Bold.woff b/src/front/fonts/FiraSans-Bold.woff
new file mode 100644
index 0000000..ebc183e
Binary files /dev/null and b/src/front/fonts/FiraSans-Bold.woff differ
diff --git a/src/front/fonts/FiraSans-Light.eot b/src/front/fonts/FiraSans-Light.eot
new file mode 100644
index 0000000..4b8c121
Binary files /dev/null and b/src/front/fonts/FiraSans-Light.eot differ
diff --git a/src/front/fonts/FiraSans-Light.ttf b/src/front/fonts/FiraSans-Light.ttf
new file mode 100644
index 0000000..a6cae2f
Binary files /dev/null and b/src/front/fonts/FiraSans-Light.ttf differ
diff --git a/src/front/fonts/FiraSans-Light.woff b/src/front/fonts/FiraSans-Light.woff
new file mode 100644
index 0000000..5983735
Binary files /dev/null and b/src/front/fonts/FiraSans-Light.woff differ
diff --git a/src/front/fonts/FiraSans-Regular.eot b/src/front/fonts/FiraSans-Regular.eot
new file mode 100644
index 0000000..ab82a33
Binary files /dev/null and b/src/front/fonts/FiraSans-Regular.eot differ
diff --git a/src/front/fonts/FiraSans-Regular.ttf b/src/front/fonts/FiraSans-Regular.ttf
new file mode 100644
index 0000000..7662009
Binary files /dev/null and b/src/front/fonts/FiraSans-Regular.ttf differ
diff --git a/src/front/fonts/FiraSans-Regular.woff b/src/front/fonts/FiraSans-Regular.woff
new file mode 100644
index 0000000..0a82f0a
Binary files /dev/null and b/src/front/fonts/FiraSans-Regular.woff differ
diff --git a/src/front/header_logo.svg b/src/front/header_logo.svg
new file mode 100644
index 0000000..509bbb0
--- /dev/null
+++ b/src/front/header_logo.svg
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="256"
+   height="256"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.4 r9939"
+   sodipodi:docname="header_logo.svg"
+   inkscape:export-filename="/home/ybon/Code/js/kosmtik/logo.png"
+   inkscape:export-xdpi="90"
+   inkscape:export-ydpi="90">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1.4"
+     inkscape:cx="183.81544"
+     inkscape:cy="152.01713"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     inkscape:window-width="1366"
+     inkscape:window-height="744"
+     inkscape:window-x="0"
+     inkscape:window-y="24"
+     inkscape:window-maximized="1"
+     inkscape:snap-page="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid2985"
+       empspacing="4"
+       visible="true"
+       enabled="true"
+       snapvisiblegridlinesonly="true" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-796.36218)">
+    <path
+       sodipodi:type="arc"
+       style="fill:none;stroke:none"
+       id="path3000-1"
+       sodipodi:cx="-32"
+       sodipodi:cy="72"
+       sodipodi:rx="112"
+       sodipodi:ry="112"
+       d="m 80,72 a 112,112 0 1 1 -224,0 112,112 0 1 1 224,0 z"
+       transform="matrix(1.1377778,0,0,1.1377778,164.40889,842.44217)" />
+    <path
+       style="fill:#ffffff;fill-opacity:1;stroke:none"
+       d="M 96 4.03125 C 40.787246 18.236855 0 68.353203 0 128 C 0 187.64671 40.787246 237.76312 96 251.96875 L 96 128 L 96 4.03125 z M 96 128 L 214.25 222.59375 C 239.90724 199.18539 256 165.46775 256 128 C 256 90.532194 239.90724 56.814596 214.25 33.40625 L 96 128 z "
+       transform="translate(0,796.36218)"
+       id="path2989-4" />
+  </g>
+</svg>
diff --git a/src/front/logo.svg b/src/front/logo.svg
new file mode 100644
index 0000000..7f8159b
--- /dev/null
+++ b/src/front/logo.svg
@@ -0,0 +1,285 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="256"
+   height="256"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.5 r10040"
+   sodipodi:docname="logo.svg"
+   inkscape:export-filename="/home/ybon/Code/js/kosmtik/favicon16.png"
+   inkscape:export-xdpi="5.625"
+   inkscape:export-ydpi="5.625">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.9899495"
+     inkscape:cx="-40.619267"
+     inkscape:cy="153.72001"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     inkscape:window-width="1366"
+     inkscape:window-height="744"
+     inkscape:window-x="0"
+     inkscape:window-y="24"
+     inkscape:window-maximized="1"
+     inkscape:snap-page="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid2985"
+       empspacing="4"
+       visible="true"
+       enabled="true"
+       snapvisiblegridlinesonly="true" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-796.36218)">
+    <path
+       sodipodi:type="arc"
+       style="fill:none;stroke:#34495e;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       id="path3000"
+       sodipodi:cx="-32"
+       sodipodi:cy="72"
+       sodipodi:rx="112"
+       sodipodi:ry="112"
+       d="m 80,72 a 112,112 0 1 1 -224,0 112,112 0 1 1 224,0 z"
+       transform="matrix(1.1377778,0,0,1.1377778,701.81004,573.74161)" />
+    <path
+       style="fill:#2c3e50;fill-opacity:1;stroke:none"
+       d="m 633.40115,527.66161 0,104 104,-104 c -290.68879,0 -56,0 -104,0 z"
+       id="path2987"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cccc" />
+    <path
+       style="fill:#2c3e50;fill-opacity:1;stroke:none"
+       d="m 793.40115,551.66161 -104,104 104,128 z"
+       id="path2989"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cccc" />
+    <path
+       style="fill:#2c3e50;fill-opacity:1;stroke:none"
+       d="m 705.40115,783.66161 -64,-120 -32,120 z"
+       id="path2991"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cccc" />
+    <path
+       style="fill:#2c3e50;fill-opacity:1;stroke:none"
+       d="m 537.40115,527.66161 64,0 -24,256 -40,0 z"
+       id="rect3780"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc" />
+    <path
+       style="fill:#ffffff;fill-opacity:1;stroke:none"
+       d="m 537.40115,527.66161 0,128 c 0,-70.69245 57.30755,-128 128,-128 l -128,0 z m 128,0 c 70.69245,0 128,57.30755 128,128 l 0,-128 -128,0 z m 128,128 c 0,70.69245 -57.30755,128 -128,128 l 128,0 0,-128 z m -128,128 c -70.69245,0 -128,-57.30755 -128,-128 l 0,128 128,0 z"
+       id="rect3785"
+       inkscape:connector-curvature="0" />
+    <path
+       sodipodi:type="arc"
+       style="fill:none;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       id="path3000-1"
+       sodipodi:cx="-32"
+       sodipodi:cy="72"
+       sodipodi:rx="112"
+       sodipodi:ry="112"
+       d="m 80,72 a 112,112 0 1 1 -224,0 112,112 0 1 1 224,0 z"
+       transform="matrix(1.1377778,0,0,1.1377778,-299.59111,786.44218)" />
+    <path
+       style="fill:#2c3e50;fill-opacity:1;stroke:none"
+       d="m -208,740.36218 -160,128 160,128.00002 z"
+       id="path2989-4"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cccc" />
+    <path
+       style="fill:#2c3e50;fill-opacity:1;stroke:none"
+       d="m -464,740.36218 96,0 0,256 -96,2e-5 z"
+       id="rect3780-4"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc" />
+    <path
+       inkscape:connector-curvature="0"
+       style="fill:#ffffff;fill-opacity:1;stroke:none"
+       d="m -464,740.36218 0,128 c 0,-70.69245 57.30755,-128 128,-128 l -128,0 z m 128,0 c 70.69245,0 128,57.30755 128,128 l 0,-128 -128,0 z m 128,128 c 0,70.69245 -57.30755,128.00002 -128,128.00002 l 128,0 0,-128.00002 z M -336,996.3622 c -70.69245,0 -128,-57.30757 -128,-128.00002 l 0,128.00002 128,0 z"
+       id="rect3785-0" />
+    <path
+       sodipodi:type="arc"
+       style="fill:none;stroke:#000000;stroke-opacity:1"
+       id="path3000-1-2"
+       sodipodi:cx="-32"
+       sodipodi:cy="72"
+       sodipodi:rx="112"
+       sodipodi:ry="112"
+       d="m 80,72 a 112,112 0 1 1 -224,0 112,112 0 1 1 224,0 z"
+       transform="matrix(1.1377778,0,0,1.1377778,-379.59111,1218.4422)" />
+    <path
+       style="fill:#2c3e50;fill-opacity:1;stroke:none"
+       d="m -288,1172.3622 -160,128 160,128 z"
+       id="path2989-4-1"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cccc" />
+    <path
+       style="fill:#2c3e50;fill-opacity:1;stroke:none"
+       d="m -544,1172.3622 96,0 0,256 -96,0 z"
+       id="rect3780-4-2"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc" />
+    <path
+       inkscape:connector-curvature="0"
+       style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-opacity:1"
+       d="m -544,1172.3622 0,128 c 0,-70.6925 57.30755,-128 128,-128 l -128,0 z m 128,0 c 70.69245,0 128,57.3075 128,128 l 0,-128 -128,0 z m 128,128 c 0,70.6924 -57.30755,128 -128,128 l 128,0 0,-128 z m -128,128 c -70.69245,0 -128,-57.3076 -128,-128 l 0,128 128,0 z"
+       id="rect3785-0-4" />
+    <path
+       sodipodi:type="arc"
+       style="fill:none;stroke:#2c3e50;stroke-opacity:1;stroke-width:1.75781246999999996;stroke-miterlimit:4;stroke-dasharray:none"
+       id="path3000-1-2-5"
+       sodipodi:cx="-32"
+       sodipodi:cy="72"
+       sodipodi:rx="112"
+       sodipodi:ry="112"
+       d="m 80,72 a 112,112 0 1 1 -224,0 112,112 0 1 1 224,0 z"
+       transform="matrix(1.1377778,0,0,1.1377778,-363.59111,1522.4422)" />
+    <path
+       style="fill:#2c3e50;fill-opacity:1;stroke:none"
+       d="m -280,1732.3622 0,-32 -144,-96 -16,0 0,128 z"
+       id="path2989-4-1-6"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cccccc" />
+    <path
+       style="fill:#2c3e50;fill-opacity:1;stroke:none"
+       d="m -440,1476.3622 168,0 0,32 -152,96 -16,0 z"
+       id="rect3780-4-2-2"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cccccc" />
+    <path
+       inkscape:connector-curvature="0"
+       style="fill:#ffffff;fill-opacity:1;stroke:none"
+       d="m -528,1476.3622 0,128 c 0,-70.6925 57.30755,-128 128,-128 z m 128,0 c 70.69245,0 128,57.3075 128,128 l 0,-128 z m 128,128 c 0,70.6924 -57.30755,128 -128,128 l 128,0 z m -128,128 c -70.69245,0 -128,-57.3076 -128,-128 l 0,128 z"
+       id="rect3785-0-4-2"
+       sodipodi:nodetypes="cccccccccccccccc" />
+    <path
+       sodipodi:type="arc"
+       style="fill:none;stroke:none"
+       id="path3000-1-1"
+       sodipodi:cx="-32"
+       sodipodi:cy="72"
+       sodipodi:rx="112"
+       sodipodi:ry="112"
+       d="m 80,72 a 112,112 0 1 1 -224,0 112,112 0 1 1 224,0 z"
+       transform="matrix(1.1377778,0,0,1.1377778,-555.59111,530.44218)" />
+    <path
+       style="fill:#2c3e50;fill-opacity:1;stroke:none"
+       d="m -464,484.36218 c 0,0 -88,96 -160,128 72,40 160,128.00002 160,128.00002 z"
+       id="path2989-4-9"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cccc" />
+    <path
+       style="fill:#2c3e50;fill-opacity:1;stroke:none"
+       d="m -720,484.36218 96,0 0,256.00002 -96,0 z"
+       id="rect3780-4-3"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc" />
+    <path
+       inkscape:connector-curvature="0"
+       style="fill:#ffffff;fill-opacity:1;stroke:none"
+       d="m -720,484.36218 0,128 c 0,-70.69245 57.30755,-128 128,-128 l -128,0 z m 128,0 c 70.69245,0 128,57.30755 128,128 l 0,-128 -128,0 z m 128,128 c 0,70.69245 -57.30755,128.00002 -128,128.00002 l 128,0 0,-128.00002 z M -592,740.3622 c -70.69245,0 -128,-57.30757 -128,-128.00002 l 0,128.00002 128,0 z"
+       id="rect3785-0-2" />
+    <path
+       sodipodi:type="arc"
+       style="fill:none;stroke:none"
+       id="path3000-1-1-8"
+       sodipodi:cx="-32"
+       sodipodi:cy="72"
+       sodipodi:rx="112"
+       sodipodi:ry="112"
+       d="m 80,72 a 112,112 0 1 1 -224,0 112,112 0 1 1 224,0 z"
+       transform="matrix(1.1377778,0,0,1.1377778,-203.59111,442.44216)" />
+    <path
+       style="fill:#2c3e50;fill-opacity:1;stroke:none"
+       d="m -112,396.36216 c 0,0 -88,96 -160,128 72,40 160,128.00002 160,128.00002 z"
+       id="path2989-4-9-1"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cccc" />
+    <path
+       style="fill:#2c3e50;fill-opacity:1;stroke:none"
+       d="m -368,396.36216 88,2e-5 c 16,112.00002 16,144 0,256 l -88,0 z"
+       id="rect3780-4-3-6"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc" />
+    <path
+       inkscape:connector-curvature="0"
+       style="fill:#ffffff;fill-opacity:1;stroke:none"
+       d="m -368,396.36216 0,128 c 0,-70.69245 57.30755,-128 128,-128 l -128,0 z m 128,0 c 70.69245,0 128,57.30755 128,128 l 0,-128 -128,0 z m 128,128 c 0,70.69245 -57.30755,128.00002 -128,128.00002 l 128,0 0,-128.00002 z m -128,128.00002 c -70.69245,0 -128,-57.30757 -128,-128.00002 l 0,128.00002 128,0 z"
+       id="rect3785-0-2-3" />
+    <path
+       sodipodi:type="arc"
+       style="fill:none;stroke:none"
+       id="path3000-1-3"
+       sodipodi:cx="-32"
+       sodipodi:cy="72"
+       sodipodi:rx="112"
+       sodipodi:ry="112"
+       d="m 80,72 a 112,112 0 1 1 -224,0 112,112 0 1 1 224,0 z"
+       transform="matrix(1.1377778,0,0,1.1377778,164.40889,842.44216)" />
+    <path
+       style="fill:#444444;fill-opacity:1;stroke:none"
+       d="M 112 1 C 48.855144 8.87002 0 62.723361 0 128 C 0 193.27664 48.855144 247.12997 112 255 L 112 128 L 112 1 z M 112 128 L 206.65625 228.96875 C 236.68783 205.54472 256 169.03743 256 128 C 256 86.962573 236.68783 50.455274 206.65625 27.03125 L 112 128 z "
+       id="path2989-4-6"
+       transform="translate(0,796.36218)" />
+    <path
+       sodipodi:type="arc"
+       style="fill:none;stroke:none"
+       id="path3000-1-3-2"
+       sodipodi:cx="-32"
+       sodipodi:cy="72"
+       sodipodi:rx="112"
+       sodipodi:ry="112"
+       d="m 80,72 a 112,112 0 1 1 -224,0 112,112 0 1 1 224,0 z"
+       transform="matrix(1.1377778,0,0,1.1377778,-587.02222,970.44218)" />
+    <path
+       style="fill:#444444;fill-opacity:1;stroke:none"
+       d="m -495.43111,924.36218 -24,0 -120,128.00002 120,128 24,0 z"
+       id="path2989-4-6-5"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="cccccc" />
+    <path
+       style="fill:#444444;fill-opacity:1;stroke:none"
+       d="m -751.43111,924.36218 112,0 0,256.00002 -112,0 z"
+       id="rect3780-4-1"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc" />
+    <path
+       inkscape:connector-curvature="0"
+       style="fill:#ffffff;fill-opacity:1;stroke:none"
+       d="m -751.43111,924.36218 0,128.00002 c 0,-70.69247 57.30755,-128.00002 128,-128.00002 l -128,0 z m 128,0 c 70.69245,0 128,57.30755 128,128.00002 l 0,-128.00002 -128,0 z m 128,128.00002 c 0,70.6924 -57.30755,128 -128,128 l 128,0 0,-128 z m -128,128 c -70.69245,0 -128,-57.3076 -128,-128 l 0,128 128,0 z"
+       id="rect3785-0-3" />
+  </g>
+</svg>
diff --git a/src/front/project.html b/src/front/project.html
new file mode 100644
index 0000000..aac8d73
--- /dev/null
+++ b/src/front/project.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset='utf-8'>
+  <title>Kosmtik</title>
+  %%JS%%
+  %%CSS%%
+</head>
+<body>
+  <div id='map' class="sidebar-map"></div>
+
+  <script type="text/javascript">
+    var map = new L.Kosmtik.Map();
+  </script>
+</body>
+</html>
diff --git a/src/plugins/base-exporters/Base.js b/src/plugins/base-exporters/Base.js
new file mode 100644
index 0000000..316a522
--- /dev/null
+++ b/src/plugins/base-exporters/Base.js
@@ -0,0 +1,10 @@
+var BaseExporter = function (project, options) {
+    this.project = project;
+    this.options = options;
+};
+
+BaseExporter.prototype.log = function () {
+    console.warn.apply(console, Array.prototype.concat.apply(['[Export]'], arguments));
+};
+
+exports.BaseExporter = BaseExporter;
diff --git a/src/plugins/base-exporters/MML.js b/src/plugins/base-exporters/MML.js
new file mode 100644
index 0000000..d1214b1
--- /dev/null
+++ b/src/plugins/base-exporters/MML.js
@@ -0,0 +1,14 @@
+var util = require('util'),
+    BaseExporter = require('./Base.js').BaseExporter;
+
+var MMLExporter = function (project, options) {
+    BaseExporter.call(this, project, options);
+};
+
+util.inherits(MMLExporter, BaseExporter);
+
+MMLExporter.prototype.export = function (callback) {
+    callback(null, JSON.stringify(this.project.load(), null, 4));
+};
+
+exports.Exporter = MMLExporter;
diff --git a/src/plugins/base-exporters/PNG.js b/src/plugins/base-exporters/PNG.js
new file mode 100644
index 0000000..f294205
--- /dev/null
+++ b/src/plugins/base-exporters/PNG.js
@@ -0,0 +1,66 @@
+var util = require('util'),
+    mapnik = require('mapnik'),
+    GeoUtils = require('../../back/GeoUtils.js'),
+    VectorBasedTile = require('../../back/VectorBasedTile.js').Tile,
+    BaseExporter = require('./Base.js').BaseExporter;
+
+var PNGExporter = function (project, options) {
+    BaseExporter.call(this, project, options);
+};
+
+util.inherits(PNGExporter, BaseExporter);
+
+PNGExporter.prototype.export = function (callback) {
+    this.scale = this.options.scale ? +this.options.scale : 2;
+    if (this.options.bounds) this.bounds = this.options.bounds.split(',').map(function (x) {return +x;});
+    else this.bounds = this.project.mml.bounds;
+    if (this.project.mml.source) this.renderFromVector(callback);
+    else this.render(callback);
+};
+PNGExporter.prototype.render = function (callback) {
+    var self = this;
+    var map = new mapnik.Map(+this.options.width, +this.options.height);
+    map.fromString(this.project.render(), {base: this.project.root}, function render (err, map) {
+        var projection = new mapnik.Projection(map.srs),
+            im = new mapnik.Image(+self.options.width, +self.options.height);
+        map.zoomToBox(projection.forward(self.bounds));
+        map.render(im, {scale: self.scale}, function toImage (err, im) {
+            if (err) throw err;
+            im.encode(self.options.format, callback);
+        });
+    });
+};
+
+PNGExporter.prototype.renderFromVector = function (callback) {
+    var self = this,
+        leftTop = GeoUtils.zoomLatLngToXY(this.options.zoom, this.bounds[3], this.bounds[0]),
+        rightBottom = GeoUtils.zoomLatLngToXY(this.options.zoom, this.bounds[1], this.bounds[2]),
+        floatLeftTop = GeoUtils.zoomLatLngToFloatXY(this.options.zoom, this.bounds[3], this.bounds[0]),
+        size = self.project.tileSize() * this.scale,
+        gap = [(floatLeftTop[0] - leftTop[0]) * size, (floatLeftTop[1] - leftTop[1]) * size],
+        map = new mapnik.Map(+this.options.width, +this.options.height),
+        data = [], processed = 0, toProcess = [],
+        commit = function () {
+            mapnik.blend(data, {format: 'png', width: +self.options.width, height: +self.options.height}, callback);
+        };
+    map.fromStringSync(this.project.render(), {base: this.project.root});
+    var processTile = function (x, y) {
+        var tile = new VectorBasedTile(self.options.zoom, x, y, {width: size, height: size});
+        return tile.render(self.project, map, function (err, im) {
+            if (err) throw err;
+            im.encode('png', function (err, buffer) {
+                data.push({buffer: buffer, x: (x - leftTop[0]) * size - gap[0], y: (y - leftTop[1]) * size - gap[1]});
+                if (toProcess[++processed]) processTile.apply(this, toProcess[processed]);
+                else commit();
+            });
+        });
+    };
+    for (var x = leftTop[0]; x <= rightBottom[0]; x++) {
+        for (var y = leftTop[1]; y <= rightBottom[1]; y++) {
+            toProcess.push([x, y]);
+        }
+    }
+    processTile.apply(this, toProcess[processed]);
+};
+
+exports.Exporter = PNGExporter;
diff --git a/src/plugins/base-exporters/XML.js b/src/plugins/base-exporters/XML.js
new file mode 100644
index 0000000..cc0d611
--- /dev/null
+++ b/src/plugins/base-exporters/XML.js
@@ -0,0 +1,14 @@
+var util = require('util'),
+    BaseExporter = require('./Base.js').BaseExporter;
+
+var XMLExporter = function (project, options) {
+    BaseExporter.call(this, project, options);
+};
+
+util.inherits(XMLExporter, BaseExporter);
+
+XMLExporter.prototype.export = function (callback) {
+    callback(null, this.project.render());
+};
+
+exports.Exporter = XMLExporter;
diff --git a/src/plugins/base-exporters/YAML.js b/src/plugins/base-exporters/YAML.js
new file mode 100644
index 0000000..fb50f4b
--- /dev/null
+++ b/src/plugins/base-exporters/YAML.js
@@ -0,0 +1,15 @@
+var util = require('util'),
+    BaseExporter = require('./Base.js').BaseExporter,
+    yaml = require('js-yaml');
+
+var YAMLExporter = function (project, options) {
+    BaseExporter.call(this, project, options);
+};
+
+util.inherits(YAMLExporter, BaseExporter);
+
+YAMLExporter.prototype.export = function (callback) {
+    callback(null, yaml.safeDump(this.project.load()));
+};
+
+exports.Exporter = YAMLExporter;
diff --git a/src/plugins/base-exporters/front/export.js b/src/plugins/base-exporters/front/export.js
new file mode 100644
index 0000000..51f82c6
--- /dev/null
+++ b/src/plugins/base-exporters/front/export.js
@@ -0,0 +1,278 @@
+L.Kosmtik.ExportFormatChooser = L.FormBuilder.Select.extend({
+
+    getOptions: function () {
+        return L.K.Config.exportFormats.map(function (item) {
+            return [item, item];
+        });
+    }
+
+});
+
+L.Kosmtik.ExportScaleChooser = L.FormBuilder.IntSelect.extend({
+
+    getOptions: function () {
+        return [1, 2, 3, 4, 5].map(function (item) {return [item, item];});
+    }
+
+});
+
+L.Kosmtik.ExportZoomChooser = L.FormBuilder.IntSelect.extend({
+
+    getOptions: function () {
+        var choices = [[-1, 'Current zoom']];
+        for (var i = 0; i <= (L.K.Config.project.maxZoom || 18); i++) {
+            choices.push([i, i]);
+        }
+        return choices;
+    }
+
+});
+
+L.K.Exporter = L.Class.extend({
+
+    shapeOptions: {
+        dashArray: '10,10',
+        color: '#444',
+        fillColor: '#444',
+        weight: 2,
+        opacity: 0.8,
+        fillOpacity: 0.7,
+        stroke: false
+    },
+
+    vertexOptions: {
+        icon: L.divIcon(),
+        draggable: true
+    },
+
+    params: {
+        showExtent: false,
+        format: 'png',
+        width: 500,
+        height: 500,
+        scale: 1,
+        zoom: -1
+    },
+
+    editableParams: {
+        'xml': [],
+        'mml': []
+    },
+
+    elementDefinitions: {
+        showExtent: {handler: L.K.Switch, label: 'Show export extent on the map.'},
+        width: {handler: 'IntInput', helpText: 'Width of the export, in px.'},
+        height: {handler: 'IntInput', helpText: 'Height of the export, in px.'},
+        scale: {handler: L.Kosmtik.ExportScaleChooser, helpText: 'Scale the rendered image.'}
+    },
+
+    initialize: function (map, options) {
+        L.setOptions(this, options);
+        this.map = map;
+        this.elementDefinitions.format = {handler: L.K.ExportFormatChooser, helpText: 'Choose the export format', callback: this.buildForm, callbackContext: this};
+        this.elementDefinitions.zoom = {handler: L.K.ExportZoomChooser, helpText: 'Choose the zoom to use', map: map};
+        this.initSidebar();
+        this.initExtentLayer();
+    },
+
+    initSidebar: function () {
+        var container = L.DomUtil.create('div', 'export-container'),
+            title = L.DomUtil.create('h3', '', container),
+            formContainer = L.DomUtil.create('div', '', container);
+        title.innerHTML = 'Export';
+        this.builder = new L.K.FormBuilder(this.params, []);
+        formContainer.appendChild(this.builder.build());
+        var submit = L.DomUtil.create('a', 'button', container);
+        submit.innerHTML = 'Export map';
+        L.DomEvent
+            .on(submit, 'click', L.DomEvent.stop)
+            .on(submit, 'click', function () {
+                window.open('./export/?' + this.getQueryString());
+            }, this);
+        this.buildForm();
+        this.map.sidebar.addTab({
+            label: 'Export',
+            content: container,
+            className: 'exporter'
+        });
+        this.map.sidebar.rebuild();
+        this.map.commands.add({
+            callback: this.openSidebar,
+            context: this,
+            name: 'Export: configure'
+        });
+    },
+
+    openSidebar: function () {
+        this.map.sidebar.open('.exporter');
+    },
+
+    buildForm: function () {
+        var elements = [['format', this.elementDefinitions.format]];
+        var extraElements = this.editableParams[this.params.format] || ['zoom', 'scale', 'showExtent'];
+        for (var i = 0; i < extraElements.length; i++) {
+            elements.push([extraElements[i], this.elementDefinitions[extraElements[i]]]);
+        }
+        this.builder.setFields(elements);
+        this.builder.build();
+    },
+
+    initExtentLayer: function () {
+        var center = this.map.getCenter(),
+            size = this.map.getSize();
+        this.params.width = size.x - 50;
+        this.params.height = size.y - 50;
+        this.extentLayer = L.featureGroup();
+        this.shape = L.polygon([], this.shapeOptions).addTo(this.extentLayer);
+        this.leftTop = L.marker(center, this.vertexOptions).addTo(this.extentLayer);
+        this.leftBottom = L.marker(center, this.vertexOptions).addTo(this.extentLayer);
+        this.rightBottom = L.marker(center, this.vertexOptions).addTo(this.extentLayer);
+        this.rightTop = L.marker(center, this.vertexOptions).addTo(this.extentLayer);
+        this.extentCaption = L.DomUtil.create('div', 'extent-caption', this.map._panes.markerPane);
+        this.setExtentCaptionPosition();
+        this.leftTop.on('drag', function (e) {
+            this.leftBottom.setLatLng([this.leftBottom._latlng.lat, e.target._latlng.lng]);
+            this.rightTop.setLatLng([e.target._latlng.lat, this.rightTop._latlng.lng]);
+            this.drawFromLatLngs();
+        }, this);
+        this.leftBottom.on('drag', function (e) {
+            this.leftTop.setLatLng([this.leftTop._latlng.lat, e.target._latlng.lng]);
+            this.rightBottom.setLatLng([e.target._latlng.lat, this.rightBottom._latlng.lng]);
+            this.drawFromLatLngs();
+        }, this);
+        this.rightBottom.on('drag', function (e) {
+            this.leftBottom.setLatLng([e.target._latlng.lat, this.leftBottom._latlng.lng]);
+            this.rightTop.setLatLng([this.rightTop._latlng.lat, e.target._latlng.lng]);
+            this.drawFromLatLngs();
+        }, this);
+        this.rightTop.on('drag', function (e) {
+            this.rightBottom.setLatLng([this.rightBottom._latlng.lat, e.target._latlng.lng]);
+            this.leftTop.setLatLng([e.target._latlng.lat, this.leftTop._latlng.lng]);
+            this.drawFromLatLngs();
+        }, this);
+        this.builder.on('postsync', function (e) {
+            if (e.helper.field === 'showExtent') this.toggleExtent();
+            else if (e.helper.field === 'zoom' || e.helper.field === 'scale') this.setExtentCaptionContent();
+        }, this);
+        this.drawFromCenter();
+    },
+
+    drawFromCenter: function () {
+        var centerPoint = this.map.latLngToLayerPoint(this.map.getCenter()),
+            left = centerPoint.x - this.params.width / 2,
+            right = centerPoint.x + this.params.width / 2,
+            top = centerPoint.y - this.params.height / 2,
+            bottom = centerPoint.y + this.params.height / 2,
+            leftTop = this.map.layerPointToLatLng([left, top]),
+            leftBottom = this.map.layerPointToLatLng([left, bottom]),
+            rightBottom = this.map.layerPointToLatLng([right, bottom]),
+            rightTop = this.map.layerPointToLatLng([right, top]);
+        this.drawFromLatLngs(leftTop, leftBottom, rightBottom, rightTop);
+    },
+
+    drawFromLatLngs: function (leftTop, leftBottom, rightBottom, rightTop) {
+        leftTop = leftTop || this.leftTop.getLatLng();
+        leftBottom = leftBottom || this.leftBottom.getLatLng();
+        rightBottom = rightBottom || this.rightBottom.getLatLng();
+        rightTop = rightTop || this.rightTop.getLatLng();
+        this.shape.setLatLngs([
+            [[90, -180], [-90, -180], [-90, 180], [90, 180]],
+            [leftTop, leftBottom, rightBottom, rightTop]
+        ]);
+        this.leftTop.setLatLng(leftTop);
+        this.leftBottom.setLatLng(leftBottom);
+        this.rightBottom.setLatLng(rightBottom);
+        this.rightTop.setLatLng(rightTop);
+        this.setExtentCaptionPosition();
+        this.setExtentCaptionContent();
+    },
+
+    showExtent: function () {
+        this.extentLayer.addTo(this.map);
+        if (this.getExtentSize().x) this.drawFromLatLngs();
+        else this.drawFromCenter();
+        this.map.on('zoomend', this.updateExtentSize, this);
+        this.map.on('zoomanim', this._zoomAnimation, this);
+        L.DomUtil.addClass(this.extentCaption, 'show');
+    },
+
+    hideExtent: function () {
+        this.map.removeLayer(this.extentLayer);
+        L.DomUtil.removeClass(this.extentCaption, 'show');
+        this.map.off('zoomend', this.updateExtentSize, this);
+        this.map.off('zoomanim', this._zoomAnimation, this);
+    },
+
+    toggleExtent: function () {
+        if (this.params.showExtent) this.showExtent();
+        else this.hideExtent();
+    },
+
+    setExtentCaptionPosition: function () {
+        var position = this.map.latLngToLayerPoint(this.leftBottom.getLatLng());
+        L.DomUtil.setPosition(this.extentCaption, position);
+    },
+
+    setExtentCaptionContent: function () {
+        var size = this.getExtentSize();
+        this.params.width = size.x;
+        this.params.height = size.y;
+        var params = this.computeParams();
+        this.extentCaption.innerHTML = params.width + 'px / ' + params.height + 'px';
+    },
+
+    getExtentSize: function () {
+        var topLeft = this.map.latLngToLayerPoint(this.leftTop.getLatLng()),
+            bottomRight = this.map.latLngToLayerPoint(this.rightBottom.getLatLng());
+        return L.point(Math.abs(bottomRight.x - topLeft.x), Math.abs(bottomRight.y - topLeft.y));
+    },
+
+    updateExtentSize: function () {
+        this.setExtentCaptionPosition();
+        this.setExtentCaptionContent();
+    },
+
+    toBBoxString: function () {
+        return [
+            this.leftBottom.getLatLng().lng,
+            this.leftBottom.getLatLng().lat,
+            this.rightTop.getLatLng().lng,
+            this.rightTop.getLatLng().lat
+        ];
+    },
+
+    computeParams: function () {
+        var params = L.extend({}, this.params),
+            factor;
+        params.bounds = this.toBBoxString();
+        params.width = params.width * +params.scale;
+        params.height = params.height * +params.scale;
+        if (params.zoom !== -1) {
+            factor = Math.pow(2, Math.abs(params.zoom - this.map.getZoom()));
+            if (params.zoom < this.map.getZoom()) factor = 1 / factor;
+            params.width = params.width * factor;
+            params.height = params.height * factor;
+        } else {
+            params.zoom = this.map.getZoom();
+        }
+        params.width = Math.round(params.width);
+        params.height = Math.round(params.height);
+        return params;
+    },
+
+    getQueryString: function () {
+        return L.K.buildQueryString(this.computeParams());
+    },
+
+    _zoomAnimation: function (e) {
+        var position = this.map._latLngToNewLayerPoint(this.leftBottom._latlng, e.zoom, e.center).round();
+        L.DomUtil.setPosition(this.extentCaption, position);
+    }
+
+});
+
+L.K.Map.addInitHook(function () {
+    this.whenReady(function () {
+        this.exportExtent = new L.K.Exporter(this);
+    });
+});
diff --git a/src/plugins/base-exporters/index.js b/src/plugins/base-exporters/index.js
new file mode 100644
index 0000000..c7018b8
--- /dev/null
+++ b/src/plugins/base-exporters/index.js
@@ -0,0 +1,81 @@
+var fs = require('fs'),
+    path = require('path');
+
+var BaseExporters = function (config) {
+    config.commands.export = config.opts.command('export').help('Export a project');
+    config.commands.export.option('project', {
+        position: 1,
+        help: 'Project to export.'
+    });
+    config.commands.export.option('output', {
+        help: 'Filepath to save in',
+        metavar: 'PATH'
+    });
+    config.commands.export.option('width', {
+        help: 'Width of the export',
+        metavar: 'INT',
+        default: 1000
+    });
+    config.commands.export.option('height', {
+        help: 'Height of the export',
+        metavar: 'INT',
+        default: 1000
+    });
+    config.commands.export.option('bbox', {
+        help: 'BBox to use [Default: project extent]',
+        metavar: 'minX,minY,maxX,maxY'
+    });
+    config.commands.export.option('scale', {
+        help: 'Scale the exported image',
+        metavar: 'INT',
+        default: 1
+    });
+    config.on('command:export', this.handleCommand);
+    config.registerExporter('xml', path.join(__dirname, 'XML.js'));
+    config.registerExporter('mml', path.join(__dirname, 'MML.js'));
+    config.registerExporter('yml', path.join(__dirname, 'YAML.js'));
+    config.registerExporter('yaml', path.join(__dirname, 'YAML.js'));
+    config.registerExporter('png', path.join(__dirname, 'PNG.js'));
+    config.registerExporter('png8', path.join(__dirname, 'PNG.js'));
+    config.registerExporter('png24', path.join(__dirname, 'PNG.js'));
+    config.registerExporter('png32', path.join(__dirname, 'PNG.js'));
+    config.registerExporter('png256', path.join(__dirname, 'PNG.js'));
+    config.on('parseopts', this.parseOpts);
+    config.addJS('/src/plugins/base-exporters/front/export.js');
+};
+
+BaseExporters.prototype.parseOpts = function (e) {
+    this.commands.export.option('format', {
+        help: 'Format of the export',
+        metavar: 'FORMAT',
+        default: 'xml',
+        choices: Object.keys(this.exporters)
+    });
+};
+
+BaseExporters.prototype.handleCommand = function () {
+    var self = this;
+    if (this.parsed_opts.project) {
+        var callback;
+        if (this.parsed_opts.output) {
+            callback = function (err, buffer) {
+                fs.writeFile(self.parsed_opts.output, buffer, function done () {
+                    console.log('Exported project to', self.parsed_opts.output);
+                });
+            };
+        } else {
+            callback = function (err, buffer) {
+                process.stdout.write(buffer);
+            };
+        }
+        var Project = require(path.join(this.root, 'src/back/Project.js')).Project,
+            project = new Project(this, this.parsed_opts.project),
+            options = this.parsed_opts;
+        project.when('loaded', function () {
+            project.export(options, callback);
+        });
+        project.load();
+    }
+};
+
+exports.Plugin = BaseExporters;
diff --git a/src/plugins/datasource-loader/index.js b/src/plugins/datasource-loader/index.js
new file mode 100644
index 0000000..aa0c975
--- /dev/null
+++ b/src/plugins/datasource-loader/index.js
@@ -0,0 +1,63 @@
+var path = require('path'),
+    Project = require(path.join(kosmtik.src, 'back/Project.js')).Project,
+    Utils = require(path.join(kosmtik.src, 'back/Utils.js'));
+
+var log = function () {
+    console.warn.apply(console, Array.prototype.concat.apply(['[datasource loader]'], arguments));
+};
+
+var DataSourceLoader = function (config) {
+    config.beforeState('project:loaded', this.patchMML.bind(this));
+};
+
+DataSourceLoader.prototype.patchMML = function (e) {
+    if (!e.project.mml) return e.continue();
+    var processed = 0, self = this,
+        sources = e.project.mml.source,
+        commit = function () {if (++processed === sources.length) e.continue();},
+        requestTileJSON = function (source) {
+            e.project.config.helpers.request({uri: source.tilejson}, function (err, resp, body) {
+                if (err) throw err;
+                var json = JSON.parse(body);
+                self.processTileJSON(source, json);
+                commit();
+            });
+        };
+    if (sources && sources.length) {
+        for (var i = 0; i < sources.length; i++) {
+            if (sources[i].protocol === 'tmsource:') {
+                this.loadLocalSource.bind(this)(sources[i], e.project.config);
+                commit();
+            } else if (sources[i].tilejson) {
+                requestTileJSON(sources[i]);
+            }
+        }
+    }
+    e.continue();
+};
+
+DataSourceLoader.prototype.loadLocalSource = function (source, config) {
+    log('Loading source from', source.path);
+    var filepath = source.path,
+        ext = path.extname(filepath);
+    if (ext !== '.yml') filepath = path.join(filepath, 'data.yml');
+    var project = new Project(config, filepath);
+    this.attachSourceUrl(source, project);
+    config.server.registerProject(project);
+};
+
+DataSourceLoader.prototype.attachSourceUrl = function (source, project) {
+    var params = {
+        host: project.config.parsed_opts.host,
+        port: project.config.parsed_opts.port,
+        path: 'tile/{z}/{x}/{y}.pbf',
+        id: project.id
+    };
+    source.url = Utils.template('http://{host}:{port}/{id}/{path}', params);
+};
+
+DataSourceLoader.prototype.processTileJSON = function (source, tilejson) {
+    source.url = tilejson.tiles[0];
+};
+
+exports.Plugin = DataSourceLoader;
diff --git a/src/plugins/hash/index.js b/src/plugins/hash/index.js
new file mode 100644
index 0000000..cd20a94
--- /dev/null
+++ b/src/plugins/hash/index.js
@@ -0,0 +1,27 @@
+var Hash = function (config) {
+    config.addJS('/node_modules/leaflet-hash/leaflet-hash.js');
+    config.addJS('/hash.js');
+    config.on('server:init', this.attachRoutes.bind(this));
+};
+
+Hash.prototype.extendMap = function (req, res) {
+
+    var front = function () {
+        L.K.Map.addInitHook(function () {
+            this.hash = new L.Hash(this);
+            if (this.hash.parseHash(location.hash)) {
+                this.hash.update();  // Do not wait for first setTimeout;
+            } else {
+                if (L.K.Config.project) this.setView(L.K.Config.project.center, L.K.Config.project.zoom);
+                else console.error('Missing center and zoom in project config');
+            }
+        });
+    };
+    this.pushToFront(res, front);
+};
+
+Hash.prototype.attachRoutes = function (e) {
+    e.server.addRoute('/hash.js', this.extendMap);
+};
+
+exports.Plugin = Hash;
diff --git a/src/plugins/local-config/index.js b/src/plugins/local-config/index.js
new file mode 100644
index 0000000..1723606
--- /dev/null
+++ b/src/plugins/local-config/index.js
@@ -0,0 +1,57 @@
+var fs = require('fs'),
+    path = require('path'),
+    Localizer = require('json-localizer').Localizer;
+
+var LocalConfig = function (config) {
+    config.opts.option('localconfig', {help: 'Path to local config file [Default: {projectpath}/localconfig.json|.js]'});
+    config.beforeState('project:loaded', this.patchMML);
+};
+
+LocalConfig.prototype.patchMML = function (e) {
+    var filepath = this.config.parsed_opts.localconfig,
+        done = function () {
+            e.project.emitAndForward('localconfig:done', e);
+            e.continue();
+        },
+        error = function (err) {
+            console.warn('[Local Config] Unable to load local config from', filepath);
+            console.error(err);
+        };
+    if (!filepath) {
+        filepath = path.join(e.project.root, 'localconfig.json');
+        if (!fs.existsSync(filepath)) {
+            // Do we have a js module instead?
+            filepath = path.join(e.project.root, 'localconfig.js');
+        }
+    }
+    // path.isAbsolute is Node 0.12 only
+    if (path.isAbsolute && !path.isAbsolute(filepath)) filepath = path.join(process.cwd(), filepath);
+    if (!fs.existsSync(filepath)) {
+        error(new Error('File not found: ' + filepath))
+        return done();  // Nothing to do;
+    }
+    var l = new Localizer(e.project.mml),
+        ext = path.extname(filepath);
+    if (ext === '.js') {
+        try {
+            new require(filepath).LocalConfig(l, e.project);
+            console.warn('[Local Config] Patched config from', filepath);
+        } catch (err) {
+            error(err);
+        }
+        done();
+    } else {
+        fs.readFile(filepath, 'utf-8', function (err, data) {
+            if (err) error(err);
+            try {
+                l.fromString(data);
+                console.warn('[Local Config]', 'Patched config from', filepath);
+            } catch (err) {
+                error(err);
+            }
+            done();
+        });
+    }
+};
+
+exports.Plugin = LocalConfig;
diff --git a/test/config.js b/test/config.js
new file mode 100644
index 0000000..96c3a5a
--- /dev/null
+++ b/test/config.js
@@ -0,0 +1,16 @@
+var Config = require('../src/Config.js').Config,
+    assert = require('assert');
+
+describe('#Config()', function () {
+
+    it('should initialize user config even without configpath', function () {
+        var config = new Config(__dirname);
+        assert(config.userConfig);
+    });
+
+    it('should initialize user config even with wrong configpath', function () {
+        var config = new Config(__dirname, 'xxx/yyy');
+        assert(config.userConfig);
+    });
+
+});
diff --git a/test/config.yml b/test/config.yml
new file mode 100644
index 0000000..e69de29
diff --git a/test/data/expected/tile.world.0.0.0.png b/test/data/expected/tile.world.0.0.0.png
new file mode 100644
index 0000000..b40de22
Binary files /dev/null and b/test/data/expected/tile.world.0.0.0.png differ
diff --git a/test/data/expected/tile.world.6.19.28.geojson b/test/data/expected/tile.world.6.19.28.geojson
new file mode 100644
index 0000000..9485eb9
--- /dev/null
+++ b/test/data/expected/tile.world.6.19.28.geojson
@@ -0,0 +1,2 @@
+{"type":"FeatureCollection","features":[{"type":"Feature","id":45,"geometry":{"type":"Polygon","coordinates":[[[-71.5869140625,19.8842660678498],[-71.7118835449219,19.7150002482043],[-71.6253662109375,19.1698157235562],[-71.7008972167969,18.7854171820721],[-71.9453430175781,18.6163147252535],[-71.6871643066406,18.3167220253372],[-71.707763671875,18.0453384889763],[-71.6583251953125,17.7578419008997],[-71.400146484375,17.5982121026292],[-71.0005187988281,18.2828222069095],[-70.66955566406 [...]
+,{"type":"Feature","id":71,"geometry":{"type":"Polygon","coordinates":[[[-73.125,19.9100923125452],[-73.125,19.5675542016562],[-72.784423828125,19.4834236041568],[-72.7912902832031,19.1010529088343],[-72.3353576660156,18.6683642351914],[-72.6951599121094,18.4457412498408],[-73.125,18.4913309691748],[-73.125,18.1784735475652],[-72.8448486328125,18.1458517716945],[-72.3724365234375,18.2150026968228],[-71.707763671875,18.0453384889763],[-71.6871643066406,18.3167220253372],[-71.9453430175781 [...]
diff --git a/test/data/expected/tile.world.6.19.28.pbf b/test/data/expected/tile.world.6.19.28.pbf
new file mode 100644
index 0000000..a0307c3
Binary files /dev/null and b/test/data/expected/tile.world.6.19.28.pbf differ
diff --git a/test/data/expected/tile.world.6.19.28.png b/test/data/expected/tile.world.6.19.28.png
new file mode 100644
index 0000000..e76c199
Binary files /dev/null and b/test/data/expected/tile.world.6.19.28.png differ
diff --git a/test/data/minimalist-project.mml b/test/data/minimalist-project.mml
new file mode 100644
index 0000000..7466ada
--- /dev/null
+++ b/test/data/minimalist-project.mml
@@ -0,0 +1,40 @@
+{
+  "bounds": [
+    1.2219,
+    43.0923,
+    1.3057,
+    43.1517
+  ],
+  "center": [
+    1.256,
+    43.1249,
+    16
+  ],
+  "minzoom": 6,
+  "maxzoom": 20,
+  "srs": "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over",
+  "Stylesheet": [],
+  "Layer": [
+    {
+      "Datasource": {
+        "file": "land-low.shp",
+        "type": "shape"
+      },
+      "class": "shp",
+      "geometry": "polygon",
+      "extent": [
+        -179.99999692067183,
+        -84.96651228427099,
+        179.99999692067183,
+        84.96651228427098
+      ],
+      "id": "land-low",
+      "name": "land-low",
+      "srs": "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over",
+      "srs-name": "900913"
+    }
+  ],
+  "scale": 1,
+  "metatile": 2,
+  "name": "ProjectName"
+}
diff --git a/test/data/minimalist-project.xml b/test/data/minimalist-project.xml
new file mode 100644
index 0000000..2c678bb
--- /dev/null
+++ b/test/data/minimalist-project.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE Map[]>
+<Map srs="+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over">
+
+<Parameters>
+  <Parameter name="bounds">1.2219,43.0923,1.3057,43.1517</Parameter>
+  <Parameter name="center">1.256,43.1249,16</Parameter>
+  <Parameter name="minzoom">6</Parameter>
+  <Parameter name="maxzoom">20</Parameter>
+  <Parameter name="scale">1</Parameter>
+  <Parameter name="metatile">2</Parameter>
+  <Parameter name="name"><![CDATA[ProjectName]]></Parameter>
+</Parameters>
+
+
+<Layer name="land-low"
+  srs="+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over">
+    
+    <Datasource>
+       <Parameter name="file"><![CDATA[land-low.shp]]></Parameter>
+       <Parameter name="type"><![CDATA[shape]]></Parameter>
+    </Datasource>
+  </Layer>
+
+</Map>
diff --git a/test/data/minimalist-project.yml b/test/data/minimalist-project.yml
new file mode 100644
index 0000000..aa41f21
--- /dev/null
+++ b/test/data/minimalist-project.yml
@@ -0,0 +1,31 @@
+bounds:
+  - 1.2219
+  - 43.0923
+  - 1.3057
+  - 43.1517
+center:
+  - 1.256
+  - 43.1249
+  - 16
+minzoom: 6
+maxzoom: 20
+srs: "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over"
+Stylesheet: []
+Layer:
+  - Datasource:
+      - file: land-low.shp
+      - type: shape
+    class: shp
+    geometry: polygon
+    extent:
+      - -179.99999692067183
+      - -84.96651228427099
+      - 179.99999692067183
+      - 84.96651228427098
+    id: land-low
+    name: land-low
+    srs: "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over"
+    srs-name: 900913
+scale: 1
+metatile: 2
+name: ProjectName
diff --git a/test/data/tree/afile.txt b/test/data/tree/afile.txt
new file mode 100644
index 0000000..e69de29
diff --git a/test/data/tree/subdir/anotherfile.js b/test/data/tree/subdir/anotherfile.js
new file mode 100644
index 0000000..e69de29
diff --git a/test/data/tree/subdir/anothersubdir/yetafile.csv b/test/data/tree/subdir/anothersubdir/yetafile.csv
new file mode 100644
index 0000000..e69de29
diff --git a/test/data/world/project.yml b/test/data/world/project.yml
new file mode 100644
index 0000000..e63ec9c
--- /dev/null
+++ b/test/data/world/project.yml
@@ -0,0 +1,35 @@
+scale: 1
+metatile: 1
+name: "The World"
+bounds:
+  - -180
+  - -85.05112877980659
+  - 180
+  - 85.05112877980659
+center:
+  - 0
+  - 0
+  - 0
+format: "png"
+minzoom: 0
+maxzoom: 20
+srs: "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over"
+Stylesheet:
+  - "style.mss"
+Layer:
+  - id: "world"
+    name: "world"
+    class: ""
+    geometry: "polygon"
+    extent:
+      - -180
+      - -85.05112877980659
+      - 180
+      - 85.05112877980659
+    srs-name: "900913"
+    srs: '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'
+    Datasource:
+      file: "world.geojson"
+      type: "ogr"
+      layer: "OGRGeoJSON"
+    advanced: {}
diff --git a/test/data/world/style.mss b/test/data/world/style.mss
new file mode 100644
index 0000000..743daf0
--- /dev/null
+++ b/test/data/world/style.mss
@@ -0,0 +1,8 @@
+Map {
+    background-color: steelblue;
+    buffer-size: 256;
+}
+
+#world {
+    polygon-fill: #f0f1fe;
+}
diff --git a/test/data/world/world.geojson b/test/data/world/world.geojson
new file mode 100644
index 0000000..5f4f7a5
--- /dev/null
+++ b/test/data/world/world.geojson
@@ -0,0 +1,179 @@
+{"type":"FeatureCollection","features":[
+{"type":"Feature","properties":{"name":"Afghanistan"},"geometry":{"type":"Polygon","coordinates":[[[61.210817,35.650072],[62.230651,35.270664],[62.984662,35.404041],[63.193538,35.857166],[63.982896,36.007957],[64.546479,36.312073],[64.746105,37.111818],[65.588948,37.305217],[65.745631,37.661164],[66.217385,37.39379],[66.518607,37.362784],[67.075782,37.356144],[67.83,37.144994],[68.135562,37.023115],[68.859446,37.344336],[69.196273,37.151144],[69.518785,37.608997],[70.116578,37.588223],[7 [...]
+{"type":"Feature","properties":{"name":"Angola"},"geometry":{"type":"MultiPolygon","coordinates":[[[[16.326528,-5.87747],[16.57318,-6.622645],[16.860191,-7.222298],[17.089996,-7.545689],[17.47297,-8.068551],[18.134222,-7.987678],[18.464176,-7.847014],[19.016752,-7.988246],[19.166613,-7.738184],[19.417502,-7.155429],[20.037723,-7.116361],[20.091622,-6.94309],[20.601823,-6.939318],[20.514748,-7.299606],[21.728111,-7.290872],[21.746456,-7.920085],[21.949131,-8.305901],[21.801801,-8.908707], [...]
+{"type":"Feature","properties":{"name":"Albania"},"geometry":{"type":"Polygon","coordinates":[[[20.590247,41.855404],[20.463175,41.515089],[20.605182,41.086226],[21.02004,40.842727],[20.99999,40.580004],[20.674997,40.435],[20.615,40.110007],[20.150016,39.624998],[19.98,39.694993],[19.960002,39.915006],[19.406082,40.250773],[19.319059,40.72723],[19.40355,41.409566],[19.540027,41.719986],[19.371769,41.877548],[19.304486,42.195745],[19.738051,42.688247],[19.801613,42.500093],[20.0707,42.588 [...]
+{"type":"Feature","properties":{"name":"United Arab Emirates"},"geometry":{"type":"Polygon","coordinates":[[[51.579519,24.245497],[51.757441,24.294073],[51.794389,24.019826],[52.577081,24.177439],[53.404007,24.151317],[54.008001,24.121758],[54.693024,24.797892],[55.439025,25.439145],[56.070821,26.055464],[56.261042,25.714606],[56.396847,24.924732],[55.886233,24.920831],[55.804119,24.269604],[55.981214,24.130543],[55.528632,23.933604],[55.525841,23.524869],[55.234489,23.110993],[55.208341 [...]
+{"type":"Feature","properties":{"name":"Argentina"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-65.5,-55.2],[-66.45,-55.25],[-66.95992,-54.89681],[-67.56244,-54.87001],[-68.63335,-54.8695],[-68.63401,-52.63637],[-68.25,-53.1],[-67.75,-53.85],[-66.45,-54.45],[-65.05,-54.7],[-65.5,-55.2]]],[[[-64.964892,-22.075862],[-64.377021,-22.798091],[-63.986838,-21.993644],[-62.846468,-22.034985],[-62.685057,-22.249029],[-60.846565,-23.880713],[-60.028966,-24.032796],[-58.807128,-24.771459], [...]
+{"type":"Feature","properties":{"name":"Armenia"},"geometry":{"type":"Polygon","coordinates":[[[43.582746,41.092143],[44.97248,41.248129],[45.179496,40.985354],[45.560351,40.81229],[45.359175,40.561504],[45.891907,40.218476],[45.610012,39.899994],[46.034534,39.628021],[46.483499,39.464155],[46.50572,38.770605],[46.143623,38.741201],[45.735379,39.319719],[45.739978,39.473999],[45.298145,39.471751],[45.001987,39.740004],[44.79399,39.713003],[44.400009,40.005],[43.656436,40.253564],[43.7526 [...]
+{"type":"Feature","properties":{"name":"Antarctica"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-59.572095,-80.040179],[-59.865849,-80.549657],[-60.159656,-81.000327],[-62.255393,-80.863178],[-64.488125,-80.921934],[-65.741666,-80.588827],[-65.741666,-80.549657],[-66.290031,-80.255773],[-64.037688,-80.294944],[-61.883246,-80.39287],[-61.138976,-79.981371],[-60.610119,-79.628679],[-59.572095,-80.040179]]],[[[-159.208184,-79.497059],[-161.127601,-79.634209],[-162.439847,-79.281465 [...]
+{"type":"Feature","properties":{"name":"French Southern and Antarctic Lands"},"geometry":{"type":"Polygon","coordinates":[[[68.935,-48.625],[69.58,-48.94],[70.525,-49.065],[70.56,-49.255],[70.28,-49.71],[68.745,-49.775],[68.72,-49.2425],[68.8675,-48.83],[68.935,-48.625]]]},"id":"ATF"},
+{"type":"Feature","properties":{"name":"Australia"},"geometry":{"type":"MultiPolygon","coordinates":[[[[145.397978,-40.792549],[146.364121,-41.137695],[146.908584,-41.000546],[147.689259,-40.808258],[148.289068,-40.875438],[148.359865,-42.062445],[148.017301,-42.407024],[147.914052,-43.211522],[147.564564,-42.937689],[146.870343,-43.634597],[146.663327,-43.580854],[146.048378,-43.549745],[145.43193,-42.693776],[145.29509,-42.03361],[144.718071,-41.162552],[144.743755,-40.703975],[145.397 [...]
+{"type":"Feature","properties":{"name":"Austria"},"geometry":{"type":"Polygon","coordinates":[[[16.979667,48.123497],[16.903754,47.714866],[16.340584,47.712902],[16.534268,47.496171],[16.202298,46.852386],[16.011664,46.683611],[15.137092,46.658703],[14.632472,46.431817],[13.806475,46.509306],[12.376485,46.767559],[12.153088,47.115393],[11.164828,46.941579],[11.048556,46.751359],[10.442701,46.893546],[9.932448,46.920728],[9.47997,47.10281],[9.632932,47.347601],[9.594226,47.525058],[9.8960 [...]
+{"type":"Feature","properties":{"name":"Azerbaijan"},"geometry":{"type":"MultiPolygon","coordinates":[[[[45.001987,39.740004],[45.298145,39.471751],[45.739978,39.473999],[45.735379,39.319719],[46.143623,38.741201],[45.457722,38.874139],[44.952688,39.335765],[44.79399,39.713003],[45.001987,39.740004]]],[[[47.373315,41.219732],[47.815666,41.151416],[47.987283,41.405819],[48.584353,41.80887],[49.110264,41.282287],[49.618915,40.572924],[50.08483,40.526157],[50.392821,40.256561],[49.569202,40 [...]
+{"type":"Feature","properties":{"name":"Burundi"},"geometry":{"type":"Polygon","coordinates":[[[29.339998,-4.499983],[29.276384,-3.293907],[29.024926,-2.839258],[29.632176,-2.917858],[29.938359,-2.348487],[30.469696,-2.413858],[30.527677,-2.807632],[30.743013,-3.034285],[30.752263,-3.35933],[30.50556,-3.568567],[30.116333,-4.090138],[29.753512,-4.452389],[29.339998,-4.499983]]]},"id":"BDI"},
+{"type":"Feature","properties":{"name":"Belgium"},"geometry":{"type":"Polygon","coordinates":[[[3.314971,51.345781],[4.047071,51.267259],[4.973991,51.475024],[5.606976,51.037298],[6.156658,50.803721],[6.043073,50.128052],[5.782417,50.090328],[5.674052,49.529484],[4.799222,49.985373],[4.286023,49.907497],[3.588184,50.378992],[3.123252,50.780363],[2.658422,50.796848],[2.513573,51.148506],[3.314971,51.345781]]]},"id":"BEL"},
+{"type":"Feature","properties":{"name":"Benin"},"geometry":{"type":"Polygon","coordinates":[[[2.691702,6.258817],[1.865241,6.142158],[1.618951,6.832038],[1.664478,9.12859],[1.463043,9.334624],[1.425061,9.825395],[1.077795,10.175607],[0.772336,10.470808],[0.899563,10.997339],[1.24347,11.110511],[1.447178,11.547719],[1.935986,11.64115],[2.154474,11.94015],[2.490164,12.233052],[2.848643,12.235636],[3.61118,11.660167],[3.572216,11.327939],[3.797112,10.734746],[3.60007,10.332186],[3.705438,10 [...]
+{"type":"Feature","properties":{"name":"Burkina Faso"},"geometry":{"type":"Polygon","coordinates":[[[-2.827496,9.642461],[-3.511899,9.900326],[-3.980449,9.862344],[-4.330247,9.610835],[-4.779884,9.821985],[-4.954653,10.152714],[-5.404342,10.370737],[-5.470565,10.95127],[-5.197843,11.375146],[-5.220942,11.713859],[-4.427166,12.542646],[-4.280405,13.228444],[-4.006391,13.472485],[-3.522803,13.337662],[-3.103707,13.541267],[-2.967694,13.79815],[-2.191825,14.246418],[-2.001035,14.559008],[-1 [...]
+{"type":"Feature","properties":{"name":"Bangladesh"},"geometry":{"type":"Polygon","coordinates":[[[92.672721,22.041239],[92.652257,21.324048],[92.303234,21.475485],[92.368554,20.670883],[92.082886,21.192195],[92.025215,21.70157],[91.834891,22.182936],[91.417087,22.765019],[90.496006,22.805017],[90.586957,22.392794],[90.272971,21.836368],[89.847467,22.039146],[89.70205,21.857116],[89.418863,21.966179],[89.031961,22.055708],[88.876312,22.879146],[88.52977,23.631142],[88.69994,24.233715],[8 [...]
+{"type":"Feature","properties":{"name":"Bulgaria"},"geometry":{"type":"Polygon","coordinates":[[[22.65715,44.234923],[22.944832,43.823785],[23.332302,43.897011],[24.100679,43.741051],[25.569272,43.688445],[26.065159,43.943494],[27.2424,44.175986],[27.970107,43.812468],[28.558081,43.707462],[28.039095,43.293172],[27.673898,42.577892],[27.99672,42.007359],[27.135739,42.141485],[26.117042,41.826905],[26.106138,41.328899],[25.197201,41.234486],[24.492645,41.583896],[23.692074,41.309081],[22. [...]
+{"type":"Feature","properties":{"name":"The Bahamas"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-77.53466,23.75975],[-77.78,23.71],[-78.03405,24.28615],[-78.40848,24.57564],[-78.19087,25.2103],[-77.89,25.17],[-77.54,24.34],[-77.53466,23.75975]]],[[[-77.82,26.58],[-78.91,26.42],[-78.98,26.79],[-78.51,26.87],[-77.85,26.84],[-77.82,26.58]]],[[[-77,26.59],[-77.17255,25.87918],[-77.35641,26.00735],[-77.34,26.53],[-77.78802,26.92516],[-77.79,27.04],[-77,26.59]]]]},"id":"BHS"},
+{"type":"Feature","properties":{"name":"Bosnia and Herzegovina"},"geometry":{"type":"Polygon","coordinates":[[[19.005486,44.860234],[19.36803,44.863],[19.11761,44.42307],[19.59976,44.03847],[19.454,43.5681],[19.21852,43.52384],[19.03165,43.43253],[18.70648,43.20011],[18.56,42.65],[17.674922,43.028563],[17.297373,43.446341],[16.916156,43.667722],[16.456443,44.04124],[16.23966,44.351143],[15.750026,44.818712],[15.959367,45.233777],[16.318157,45.004127],[16.534939,45.211608],[17.002146,45.2 [...]
+{"type":"Feature","properties":{"name":"Belarus"},"geometry":{"type":"Polygon","coordinates":[[[23.484128,53.912498],[24.450684,53.905702],[25.536354,54.282423],[25.768433,54.846963],[26.588279,55.167176],[26.494331,55.615107],[27.10246,55.783314],[28.176709,56.16913],[29.229513,55.918344],[29.371572,55.670091],[29.896294,55.789463],[30.873909,55.550976],[30.971836,55.081548],[30.757534,54.811771],[31.384472,54.157056],[31.791424,53.974639],[31.731273,53.794029],[32.405599,53.618045],[32 [...]
+{"type":"Feature","properties":{"name":"Belize"},"geometry":{"type":"Polygon","coordinates":[[[-89.14308,17.808319],[-89.150909,17.955468],[-89.029857,18.001511],[-88.848344,17.883198],[-88.490123,18.486831],[-88.300031,18.499982],[-88.296336,18.353273],[-88.106813,18.348674],[-88.123479,18.076675],[-88.285355,17.644143],[-88.197867,17.489475],[-88.302641,17.131694],[-88.239518,17.036066],[-88.355428,16.530774],[-88.551825,16.265467],[-88.732434,16.233635],[-88.930613,15.887273],[-89.229 [...]
+{"type":"Feature","properties":{"name":"Bolivia"},"geometry":{"type":"Polygon","coordinates":[[[-62.846468,-22.034985],[-63.986838,-21.993644],[-64.377021,-22.798091],[-64.964892,-22.075862],[-66.273339,-21.83231],[-67.106674,-22.735925],[-67.82818,-22.872919],[-68.219913,-21.494347],[-68.757167,-20.372658],[-68.442225,-19.405068],[-68.966818,-18.981683],[-69.100247,-18.260125],[-69.590424,-17.580012],[-68.959635,-16.500698],[-69.389764,-15.660129],[-69.160347,-15.323974],[-69.339535,-14 [...]
+{"type":"Feature","properties":{"name":"Brazil"},"geometry":{"type":"Polygon","coordinates":[[[-57.625133,-30.216295],[-56.2909,-28.852761],[-55.162286,-27.881915],[-54.490725,-27.474757],[-53.648735,-26.923473],[-53.628349,-26.124865],[-54.13005,-25.547639],[-54.625291,-25.739255],[-54.428946,-25.162185],[-54.293476,-24.5708],[-54.29296,-24.021014],[-54.652834,-23.839578],[-55.027902,-24.001274],[-55.400747,-23.956935],[-55.517639,-23.571998],[-55.610683,-22.655619],[-55.797958,-22.3569 [...]
+{"type":"Feature","properties":{"name":"Brunei"},"geometry":{"type":"Polygon","coordinates":[[[114.204017,4.525874],[114.599961,4.900011],[115.45071,5.44773],[115.4057,4.955228],[115.347461,4.316636],[114.869557,4.348314],[114.659596,4.007637],[114.204017,4.525874]]]},"id":"BRN"},
+{"type":"Feature","properties":{"name":"Bhutan"},"geometry":{"type":"Polygon","coordinates":[[[91.696657,27.771742],[92.103712,27.452614],[92.033484,26.83831],[91.217513,26.808648],[90.373275,26.875724],[89.744528,26.719403],[88.835643,27.098966],[88.814248,27.299316],[89.47581,28.042759],[90.015829,28.296439],[90.730514,28.064954],[91.258854,28.040614],[91.696657,27.771742]]]},"id":"BTN"},
+{"type":"Feature","properties":{"name":"Botswana"},"geometry":{"type":"Polygon","coordinates":[[[25.649163,-18.536026],[25.850391,-18.714413],[26.164791,-19.293086],[27.296505,-20.39152],[27.724747,-20.499059],[27.727228,-20.851802],[28.02137,-21.485975],[28.794656,-21.639454],[29.432188,-22.091313],[28.017236,-22.827754],[27.11941,-23.574323],[26.786407,-24.240691],[26.485753,-24.616327],[25.941652,-24.696373],[25.765849,-25.174845],[25.664666,-25.486816],[25.025171,-25.71967],[24.21126 [...]
+{"type":"Feature","properties":{"name":"Central African Republic"},"geometry":{"type":"Polygon","coordinates":[[[15.27946,7.421925],[16.106232,7.497088],[16.290562,7.754307],[16.456185,7.734774],[16.705988,7.508328],[17.96493,7.890914],[18.389555,8.281304],[18.911022,8.630895],[18.81201,8.982915],[19.094008,9.074847],[20.059685,9.012706],[21.000868,9.475985],[21.723822,10.567056],[22.231129,10.971889],[22.864165,11.142395],[22.977544,10.714463],[23.554304,10.089255],[23.55725,9.681218],[ [...]
+{"type":"Feature","properties":{"name":"Canada"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-63.6645,46.55001],[-62.9393,46.41587],[-62.01208,46.44314],[-62.50391,46.03339],[-62.87433,45.96818],[-64.1428,46.39265],[-64.39261,46.72747],[-64.01486,47.03601],[-63.6645,46.55001]]],[[[-61.806305,49.10506],[-62.29318,49.08717],[-63.58926,49.40069],[-64.51912,49.87304],[-64.17322,49.95718],[-62.85829,49.70641],[-61.835585,49.28855],[-61.806305,49.10506]]],[[[-123.510002,48.510011],[-12 [...]
+{"type":"Feature","properties":{"name":"Switzerland"},"geometry":{"type":"Polygon","coordinates":[[[9.594226,47.525058],[9.632932,47.347601],[9.47997,47.10281],[9.932448,46.920728],[10.442701,46.893546],[10.363378,46.483571],[9.922837,46.314899],[9.182882,46.440215],[8.966306,46.036932],[8.489952,46.005151],[8.31663,46.163642],[7.755992,45.82449],[7.273851,45.776948],[6.843593,45.991147],[6.5001,46.429673],[6.022609,46.27299],[6.037389,46.725779],[6.768714,47.287708],[6.736571,47.541801] [...]
+{"type":"Feature","properties":{"name":"Chile"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-68.63401,-52.63637],[-68.63335,-54.8695],[-67.56244,-54.87001],[-66.95992,-54.89681],[-67.29103,-55.30124],[-68.14863,-55.61183],[-68.639991,-55.580018],[-69.2321,-55.49906],[-69.95809,-55.19843],[-71.00568,-55.05383],[-72.2639,-54.49514],[-73.2852,-53.95752],[-74.66253,-52.83749],[-73.8381,-53.04743],[-72.43418,-53.7154],[-71.10773,-54.07433],[-70.59178,-53.61583],[-70.26748,-52.93123],[ [...]
+{"type":"Feature","properties":{"name":"China"},"geometry":{"type":"MultiPolygon","coordinates":[[[[110.339188,18.678395],[109.47521,18.197701],[108.655208,18.507682],[108.626217,19.367888],[109.119056,19.821039],[110.211599,20.101254],[110.786551,20.077534],[111.010051,19.69593],[110.570647,19.255879],[110.339188,18.678395]]],[[[127.657407,49.76027],[129.397818,49.4406],[130.582293,48.729687],[130.987282,47.790132],[132.506672,47.78897],[133.373596,48.183442],[135.026311,48.47823],[134. [...]
+{"type":"Feature","properties":{"name":"Ivory Coast"},"geometry":{"type":"Polygon","coordinates":[[[-2.856125,4.994476],[-3.311084,4.984296],[-4.00882,5.179813],[-4.649917,5.168264],[-5.834496,4.993701],[-6.528769,4.705088],[-7.518941,4.338288],[-7.712159,4.364566],[-7.635368,5.188159],[-7.539715,5.313345],[-7.570153,5.707352],[-7.993693,6.12619],[-8.311348,6.193033],[-8.60288,6.467564],[-8.385452,6.911801],[-8.485446,7.395208],[-8.439298,7.686043],[-8.280703,7.68718],[-8.221792,8.123329 [...]
+{"type":"Feature","properties":{"name":"Cameroon"},"geometry":{"type":"Polygon","coordinates":[[[13.075822,2.267097],[12.951334,2.321616],[12.35938,2.192812],[11.751665,2.326758],[11.276449,2.261051],[9.649158,2.283866],[9.795196,3.073404],[9.404367,3.734527],[8.948116,3.904129],[8.744924,4.352215],[8.488816,4.495617],[8.500288,4.771983],[8.757533,5.479666],[9.233163,6.444491],[9.522706,6.453482],[10.118277,7.03877],[10.497375,7.055358],[11.058788,6.644427],[11.745774,6.981383],[11.83930 [...]
+{"type":"Feature","properties":{"name":"Democratic Republic of the Congo"},"geometry":{"type":"Polygon","coordinates":[[[30.83386,3.509166],[30.773347,2.339883],[31.174149,2.204465],[30.85267,1.849396],[30.468508,1.583805],[30.086154,1.062313],[29.875779,0.59738],[29.819503,-0.20531],[29.587838,-0.587406],[29.579466,-1.341313],[29.291887,-1.620056],[29.254835,-2.21511],[29.117479,-2.292211],[29.024926,-2.839258],[29.276384,-3.293907],[29.339998,-4.499983],[29.519987,-5.419979],[29.419993 [...]
+{"type":"Feature","properties":{"name":"Republic of the Congo"},"geometry":{"type":"Polygon","coordinates":[[[12.995517,-4.781103],[12.62076,-4.438023],[12.318608,-4.60623],[11.914963,-5.037987],[11.093773,-3.978827],[11.855122,-3.426871],[11.478039,-2.765619],[11.820964,-2.514161],[12.495703,-2.391688],[12.575284,-1.948511],[13.109619,-2.42874],[13.992407,-2.470805],[14.29921,-1.998276],[14.425456,-1.333407],[14.316418,-0.552627],[13.843321,0.038758],[14.276266,1.19693],[14.026669,1.395 [...]
+{"type":"Feature","properties":{"name":"Colombia"},"geometry":{"type":"Polygon","coordinates":[[[-75.373223,-0.152032],[-75.801466,0.084801],[-76.292314,0.416047],[-76.57638,0.256936],[-77.424984,0.395687],[-77.668613,0.825893],[-77.855061,0.809925],[-78.855259,1.380924],[-78.990935,1.69137],[-78.617831,1.766404],[-78.662118,2.267355],[-78.42761,2.629556],[-77.931543,2.696606],[-77.510431,3.325017],[-77.12769,3.849636],[-77.496272,4.087606],[-77.307601,4.667984],[-77.533221,5.582812],[-7 [...]
+{"type":"Feature","properties":{"name":"Costa Rica"},"geometry":{"type":"Polygon","coordinates":[[[-82.965783,8.225028],[-83.508437,8.446927],[-83.711474,8.656836],[-83.596313,8.830443],[-83.632642,9.051386],[-83.909886,9.290803],[-84.303402,9.487354],[-84.647644,9.615537],[-84.713351,9.908052],[-84.97566,10.086723],[-84.911375,9.795992],[-85.110923,9.55704],[-85.339488,9.834542],[-85.660787,9.933347],[-85.797445,10.134886],[-85.791709,10.439337],[-85.659314,10.754331],[-85.941725,10.895 [...]
+{"type":"Feature","properties":{"name":"Cuba"},"geometry":{"type":"Polygon","coordinates":[[[-82.268151,23.188611],[-81.404457,23.117271],[-80.618769,23.10598],[-79.679524,22.765303],[-79.281486,22.399202],[-78.347434,22.512166],[-77.993296,22.277194],[-77.146422,21.657851],[-76.523825,21.20682],[-76.19462,21.220565],[-75.598222,21.016624],[-75.67106,20.735091],[-74.933896,20.693905],[-74.178025,20.284628],[-74.296648,20.050379],[-74.961595,19.923435],[-75.63468,19.873774],[-76.323656,19 [...]
+{"type":"Feature","properties":{"name":"Northern Cyprus"},"geometry":{"type":"Polygon","coordinates":[[[32.73178,35.140026],[32.802474,35.145504],[32.946961,35.386703],[33.667227,35.373216],[34.576474,35.671596],[33.900804,35.245756],[33.973617,35.058506],[33.86644,35.093595],[33.675392,35.017863],[33.525685,35.038688],[33.475817,35.000345],[33.455922,35.101424],[33.383833,35.162712],[33.190977,35.173125],[32.919572,35.087833],[32.73178,35.140026]]]},"id":"-99"},
+{"type":"Feature","properties":{"name":"Cyprus"},"geometry":{"type":"Polygon","coordinates":[[[33.973617,35.058506],[34.004881,34.978098],[32.979827,34.571869],[32.490296,34.701655],[32.256667,35.103232],[32.73178,35.140026],[32.919572,35.087833],[33.190977,35.173125],[33.383833,35.162712],[33.455922,35.101424],[33.475817,35.000345],[33.525685,35.038688],[33.675392,35.017863],[33.86644,35.093595],[33.973617,35.058506]]]},"id":"CYP"},
+{"type":"Feature","properties":{"name":"Czech Republic"},"geometry":{"type":"Polygon","coordinates":[[[16.960288,48.596982],[16.499283,48.785808],[16.029647,48.733899],[15.253416,49.039074],[14.901447,48.964402],[14.338898,48.555305],[13.595946,48.877172],[13.031329,49.307068],[12.521024,49.547415],[12.415191,49.969121],[12.240111,50.266338],[12.966837,50.484076],[13.338132,50.733234],[14.056228,50.926918],[14.307013,51.117268],[14.570718,51.002339],[15.016996,51.106674],[15.490972,50.78 [...]
+{"type":"Feature","properties":{"name":"Germany"},"geometry":{"type":"Polygon","coordinates":[[[9.921906,54.983104],[9.93958,54.596642],[10.950112,54.363607],[10.939467,54.008693],[11.956252,54.196486],[12.51844,54.470371],[13.647467,54.075511],[14.119686,53.757029],[14.353315,53.248171],[14.074521,52.981263],[14.4376,52.62485],[14.685026,52.089947],[14.607098,51.745188],[15.016996,51.106674],[14.570718,51.002339],[14.307013,51.117268],[14.056228,50.926918],[13.338132,50.733234],[12.9668 [...]
+{"type":"Feature","properties":{"name":"Djibouti"},"geometry":{"type":"Polygon","coordinates":[[[43.081226,12.699639],[43.317852,12.390148],[43.286381,11.974928],[42.715874,11.735641],[43.145305,11.46204],[42.776852,10.926879],[42.55493,11.10511],[42.31414,11.0342],[41.75557,11.05091],[41.73959,11.35511],[41.66176,11.6312],[42,12.1],[42.35156,12.54223],[42.779642,12.455416],[43.081226,12.699639]]]},"id":"DJI"},
+{"type":"Feature","properties":{"name":"Denmark"},"geometry":{"type":"MultiPolygon","coordinates":[[[[12.690006,55.609991],[12.089991,54.800015],[11.043543,55.364864],[10.903914,55.779955],[12.370904,56.111407],[12.690006,55.609991]]],[[[10.912182,56.458621],[10.667804,56.081383],[10.369993,56.190007],[9.649985,55.469999],[9.921906,54.983104],[9.282049,54.830865],[8.526229,54.962744],[8.120311,55.517723],[8.089977,56.540012],[8.256582,56.809969],[8.543438,57.110003],[9.424469,57.172066], [...]
+{"type":"Feature","properties":{"name":"Dominican Republic"},"geometry":{"type":"Polygon","coordinates":[[[-71.712361,19.714456],[-71.587304,19.884911],[-70.806706,19.880286],[-70.214365,19.622885],[-69.950815,19.648],[-69.76925,19.293267],[-69.222126,19.313214],[-69.254346,19.015196],[-68.809412,18.979074],[-68.317943,18.612198],[-68.689316,18.205142],[-69.164946,18.422648],[-69.623988,18.380713],[-69.952934,18.428307],[-70.133233,18.245915],[-70.517137,18.184291],[-70.669298,18.426886] [...]
+{"type":"Feature","properties":{"name":"Algeria"},"geometry":{"type":"Polygon","coordinates":[[[11.999506,23.471668],[8.572893,21.565661],[5.677566,19.601207],[4.267419,19.155265],[3.158133,19.057364],[3.146661,19.693579],[2.683588,19.85623],[2.060991,20.142233],[1.823228,20.610809],[-1.550055,22.792666],[-4.923337,24.974574],[-8.6844,27.395744],[-8.665124,27.589479],[-8.66559,27.656426],[-8.674116,28.841289],[-7.059228,29.579228],[-6.060632,29.7317],[-5.242129,30.000443],[-4.859646,30.5 [...]
+{"type":"Feature","properties":{"name":"Ecuador"},"geometry":{"type":"Polygon","coordinates":[[[-80.302561,-3.404856],[-79.770293,-2.657512],[-79.986559,-2.220794],[-80.368784,-2.685159],[-80.967765,-2.246943],[-80.764806,-1.965048],[-80.933659,-1.057455],[-80.58337,-0.906663],[-80.399325,-0.283703],[-80.020898,0.36034],[-80.09061,0.768429],[-79.542762,0.982938],[-78.855259,1.380924],[-77.855061,0.809925],[-77.668613,0.825893],[-77.424984,0.395687],[-76.57638,0.256936],[-76.292314,0.4160 [...]
+{"type":"Feature","properties":{"name":"Egypt"},"geometry":{"type":"Polygon","coordinates":[[[34.9226,29.50133],[34.64174,29.09942],[34.42655,28.34399],[34.15451,27.8233],[33.92136,27.6487],[33.58811,27.97136],[33.13676,28.41765],[32.42323,29.85108],[32.32046,29.76043],[32.73482,28.70523],[33.34876,27.69989],[34.10455,26.14227],[34.47387,25.59856],[34.79507,25.03375],[35.69241,23.92671],[35.49372,23.75237],[35.52598,23.10244],[36.69069,22.20485],[36.86623,22],[32.9,22],[29.02,22],[25,22] [...]
+{"type":"Feature","properties":{"name":"Eritrea"},"geometry":{"type":"Polygon","coordinates":[[[42.35156,12.54223],[42.00975,12.86582],[41.59856,13.45209],[41.155194,13.77332],[40.8966,14.11864],[40.026219,14.519579],[39.34061,14.53155],[39.0994,14.74064],[38.51295,14.50547],[37.90607,14.95943],[37.59377,14.2131],[36.42951,14.42211],[36.323189,14.822481],[36.75386,16.291874],[36.85253,16.95655],[37.16747,17.26314],[37.904,17.42754],[38.41009,17.998307],[38.990623,16.840626],[39.26611,15. [...]
+{"type":"Feature","properties":{"name":"Spain"},"geometry":{"type":"Polygon","coordinates":[[[-9.034818,41.880571],[-8.984433,42.592775],[-9.392884,43.026625],[-7.97819,43.748338],[-6.754492,43.567909],[-5.411886,43.57424],[-4.347843,43.403449],[-3.517532,43.455901],[-1.901351,43.422802],[-1.502771,43.034014],[0.338047,42.579546],[0.701591,42.795734],[1.826793,42.343385],[2.985999,42.473015],[3.039484,41.89212],[2.091842,41.226089],[0.810525,41.014732],[0.721331,40.678318],[0.106692,40.1 [...]
+{"type":"Feature","properties":{"name":"Estonia"},"geometry":{"type":"Polygon","coordinates":[[[24.312863,57.793424],[24.428928,58.383413],[24.061198,58.257375],[23.42656,58.612753],[23.339795,59.18724],[24.604214,59.465854],[25.864189,59.61109],[26.949136,59.445803],[27.981114,59.475388],[28.131699,59.300825],[27.420166,58.724581],[27.716686,57.791899],[27.288185,57.474528],[26.463532,57.476389],[25.60281,57.847529],[25.164594,57.970157],[24.312863,57.793424]]]},"id":"EST"},
+{"type":"Feature","properties":{"name":"Ethiopia"},"geometry":{"type":"Polygon","coordinates":[[[37.90607,14.95943],[38.51295,14.50547],[39.0994,14.74064],[39.34061,14.53155],[40.02625,14.51959],[40.8966,14.11864],[41.1552,13.77333],[41.59856,13.45209],[42.00975,12.86582],[42.35156,12.54223],[42,12.1],[41.66176,11.6312],[41.73959,11.35511],[41.75557,11.05091],[42.31414,11.0342],[42.55493,11.10511],[42.776852,10.926879],[42.55876,10.57258],[42.92812,10.02194],[43.29699,9.54048],[43.67875, [...]
+{"type":"Feature","properties":{"name":"Finland"},"geometry":{"type":"Polygon","coordinates":[[[28.59193,69.064777],[28.445944,68.364613],[29.977426,67.698297],[29.054589,66.944286],[30.21765,65.80598],[29.54443,64.948672],[30.444685,64.204453],[30.035872,63.552814],[31.516092,62.867687],[31.139991,62.357693],[30.211107,61.780028],[28.069998,60.503517],[26.255173,60.423961],[24.496624,60.057316],[22.869695,59.846373],[22.290764,60.391921],[21.322244,60.72017],[21.544866,61.705329],[21.05 [...]
+{"type":"Feature","properties":{"name":"Fiji"},"geometry":{"type":"MultiPolygon","coordinates":[[[[178.3736,-17.33992],[178.71806,-17.62846],[178.55271,-18.15059],[177.93266,-18.28799],[177.38146,-18.16432],[177.28504,-17.72465],[177.67087,-17.38114],[178.12557,-17.50481],[178.3736,-17.33992]]],[[[179.364143,-16.801354],[178.725059,-17.012042],[178.596839,-16.63915],[179.096609,-16.433984],[179.413509,-16.379054],[180,-16.067133],[180,-16.555217],[179.364143,-16.801354]]],[[[-179.917369, [...]
+{"type":"Feature","properties":{"name":"Falkland Islands"},"geometry":{"type":"Polygon","coordinates":[[[-61.2,-51.85],[-60,-51.25],[-59.15,-51.5],[-58.55,-51.1],[-57.75,-51.55],[-58.05,-51.9],[-59.4,-52.2],[-59.85,-51.85],[-60.7,-52.3],[-61.2,-51.85]]]},"id":"FLK"},
+{"type":"Feature","properties":{"name":"France"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-52.556425,2.504705],[-52.939657,2.124858],[-53.418465,2.053389],[-53.554839,2.334897],[-53.778521,2.376703],[-54.088063,2.105557],[-54.524754,2.311849],[-54.27123,2.738748],[-54.184284,3.194172],[-54.011504,3.62257],[-54.399542,4.212611],[-54.478633,4.896756],[-53.958045,5.756548],[-53.618453,5.646529],[-52.882141,5.409851],[-51.823343,4.565768],[-51.657797,4.156232],[-52.249338,3.241094 [...]
+{"type":"Feature","properties":{"name":"Gabon"},"geometry":{"type":"Polygon","coordinates":[[[11.093773,-3.978827],[10.066135,-2.969483],[9.405245,-2.144313],[8.797996,-1.111301],[8.830087,-0.779074],[9.04842,-0.459351],[9.291351,0.268666],[9.492889,1.01012],[9.830284,1.067894],[11.285079,1.057662],[11.276449,2.261051],[11.751665,2.326758],[12.35938,2.192812],[12.951334,2.321616],[13.075822,2.267097],[13.003114,1.830896],[13.282631,1.314184],[14.026669,1.395677],[14.276266,1.19693],[13.8 [...]
+{"type":"Feature","properties":{"name":"United Kingdom"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-5.661949,54.554603],[-6.197885,53.867565],[-6.95373,54.073702],[-7.572168,54.059956],[-7.366031,54.595841],[-7.572168,55.131622],[-6.733847,55.17286],[-5.661949,54.554603]]],[[[-3.005005,58.635],[-4.073828,57.553025],[-3.055002,57.690019],[-1.959281,57.6848],[-2.219988,56.870017],[-3.119003,55.973793],[-2.085009,55.909998],[-2.005676,55.804903],[-1.114991,54.624986],[-0.430485,54 [...]
+{"type":"Feature","properties":{"name":"Georgia"},"geometry":{"type":"Polygon","coordinates":[[[41.554084,41.535656],[41.703171,41.962943],[41.45347,42.645123],[40.875469,43.013628],[40.321394,43.128634],[39.955009,43.434998],[40.076965,43.553104],[40.922185,43.382159],[42.394395,43.220308],[43.756017,42.740828],[43.9312,42.554974],[44.537623,42.711993],[45.470279,42.502781],[45.77641,42.092444],[46.404951,41.860675],[46.145432,41.722802],[46.637908,41.181673],[46.501637,41.064445],[45.9 [...]
+{"type":"Feature","properties":{"name":"Ghana"},"geometry":{"type":"Polygon","coordinates":[[[1.060122,5.928837],[-0.507638,5.343473],[-1.063625,5.000548],[-1.964707,4.710462],[-2.856125,4.994476],[-2.810701,5.389051],[-3.24437,6.250472],[-2.983585,7.379705],[-2.56219,8.219628],[-2.827496,9.642461],[-2.963896,10.395335],[-2.940409,10.96269],[-1.203358,11.009819],[-0.761576,10.93693],[-0.438702,11.098341],[0.023803,11.018682],[-0.049785,10.706918],[0.36758,10.191213],[0.365901,9.465004],[ [...]
+{"type":"Feature","properties":{"name":"Guinea"},"geometry":{"type":"Polygon","coordinates":[[[-8.439298,7.686043],[-8.722124,7.711674],[-8.926065,7.309037],[-9.208786,7.313921],[-9.403348,7.526905],[-9.33728,7.928534],[-9.755342,8.541055],[-10.016567,8.428504],[-10.230094,8.406206],[-10.505477,8.348896],[-10.494315,8.715541],[-10.65477,8.977178],[-10.622395,9.26791],[-10.839152,9.688246],[-11.117481,10.045873],[-11.917277,10.046984],[-12.150338,9.858572],[-12.425929,9.835834],[-12.59671 [...]
+{"type":"Feature","properties":{"name":"Gambia"},"geometry":{"type":"Polygon","coordinates":[[[-16.841525,13.151394],[-16.713729,13.594959],[-15.624596,13.623587],[-15.39877,13.860369],[-15.081735,13.876492],[-14.687031,13.630357],[-14.376714,13.62568],[-14.046992,13.794068],[-13.844963,13.505042],[-14.277702,13.280585],[-14.712197,13.298207],[-15.141163,13.509512],[-15.511813,13.27857],[-15.691001,13.270353],[-15.931296,13.130284],[-16.841525,13.151394]]]},"id":"GMB"},
+{"type":"Feature","properties":{"name":"Guinea Bissau"},"geometry":{"type":"Polygon","coordinates":[[[-15.130311,11.040412],[-15.66418,11.458474],[-16.085214,11.524594],[-16.314787,11.806515],[-16.308947,11.958702],[-16.613838,12.170911],[-16.677452,12.384852],[-16.147717,12.547762],[-15.816574,12.515567],[-15.548477,12.62817],[-13.700476,12.586183],[-13.718744,12.247186],[-13.828272,12.142644],[-13.743161,11.811269],[-13.9008,11.678719],[-14.121406,11.677117],[-14.382192,11.509272],[-14 [...]
+{"type":"Feature","properties":{"name":"Equatorial Guinea"},"geometry":{"type":"Polygon","coordinates":[[[9.492889,1.01012],[9.305613,1.160911],[9.649158,2.283866],[11.276449,2.261051],[11.285079,1.057662],[9.830284,1.067894],[9.492889,1.01012]]]},"id":"GNQ"},
+{"type":"Feature","properties":{"name":"Greece"},"geometry":{"type":"MultiPolygon","coordinates":[[[[23.69998,35.705004],[24.246665,35.368022],[25.025015,35.424996],[25.769208,35.354018],[25.745023,35.179998],[26.290003,35.29999],[26.164998,35.004995],[24.724982,34.919988],[24.735007,35.084991],[23.514978,35.279992],[23.69998,35.705004]]],[[[26.604196,41.562115],[26.294602,40.936261],[26.056942,40.824123],[25.447677,40.852545],[24.925848,40.947062],[23.714811,40.687129],[24.407999,40.124 [...]
+{"type":"Feature","properties":{"name":"Greenland"},"geometry":{"type":"Polygon","coordinates":[[[-46.76379,82.62796],[-43.40644,83.22516],[-39.89753,83.18018],[-38.62214,83.54905],[-35.08787,83.64513],[-27.10046,83.51966],[-20.84539,82.72669],[-22.69182,82.34165],[-26.51753,82.29765],[-31.9,82.2],[-31.39646,82.02154],[-27.85666,82.13178],[-24.84448,81.78697],[-22.90328,82.09317],[-22.07175,81.73449],[-23.16961,81.15271],[-20.62363,81.52462],[-15.76818,81.91245],[-12.77018,81.71885],[-12 [...]
+{"type":"Feature","properties":{"name":"Guatemala"},"geometry":{"type":"Polygon","coordinates":[[[-90.095555,13.735338],[-90.608624,13.909771],[-91.23241,13.927832],[-91.689747,14.126218],[-92.22775,14.538829],[-92.20323,14.830103],[-92.087216,15.064585],[-92.229249,15.251447],[-91.74796,16.066565],[-90.464473,16.069562],[-90.438867,16.41011],[-90.600847,16.470778],[-90.711822,16.687483],[-91.08167,16.918477],[-91.453921,17.252177],[-91.002269,17.254658],[-91.00152,17.817595],[-90.067934 [...]
+{"type":"Feature","properties":{"name":"Guyana"},"geometry":{"type":"Polygon","coordinates":[[[-59.758285,8.367035],[-59.101684,7.999202],[-58.482962,7.347691],[-58.454876,6.832787],[-58.078103,6.809094],[-57.542219,6.321268],[-57.147436,5.97315],[-57.307246,5.073567],[-57.914289,4.812626],[-57.86021,4.576801],[-58.044694,4.060864],[-57.601569,3.334655],[-57.281433,3.333492],[-57.150098,2.768927],[-56.539386,1.899523],[-56.782704,1.863711],[-57.335823,1.948538],[-57.660971,1.682585],[-58 [...]
+{"type":"Feature","properties":{"name":"Honduras"},"geometry":{"type":"Polygon","coordinates":[[[-87.316654,12.984686],[-87.489409,13.297535],[-87.793111,13.38448],[-87.723503,13.78505],[-87.859515,13.893312],[-88.065343,13.964626],[-88.503998,13.845486],[-88.541231,13.980155],[-88.843073,14.140507],[-89.058512,14.340029],[-89.353326,14.424133],[-89.145535,14.678019],[-89.22522,14.874286],[-89.154811,15.066419],[-88.68068,15.346247],[-88.225023,15.727722],[-88.121153,15.688655],[-87.9018 [...]
+{"type":"Feature","properties":{"name":"Croatia"},"geometry":{"type":"Polygon","coordinates":[[[18.829838,45.908878],[19.072769,45.521511],[19.390476,45.236516],[19.005486,44.860234],[18.553214,45.08159],[17.861783,45.06774],[17.002146,45.233777],[16.534939,45.211608],[16.318157,45.004127],[15.959367,45.233777],[15.750026,44.818712],[16.23966,44.351143],[16.456443,44.04124],[16.916156,43.667722],[17.297373,43.446341],[17.674922,43.028563],[18.56,42.65],[18.450016,42.479991],[17.50997,42. [...]
+{"type":"Feature","properties":{"name":"Haiti"},"geometry":{"type":"Polygon","coordinates":[[[-73.189791,19.915684],[-72.579673,19.871501],[-71.712361,19.714456],[-71.624873,19.169838],[-71.701303,18.785417],[-71.945112,18.6169],[-71.687738,18.31666],[-71.708305,18.044997],[-72.372476,18.214961],[-72.844411,18.145611],[-73.454555,18.217906],[-73.922433,18.030993],[-74.458034,18.34255],[-74.369925,18.664908],[-73.449542,18.526053],[-72.694937,18.445799],[-72.334882,18.668422],[-72.79165,1 [...]
+{"type":"Feature","properties":{"name":"Hungary"},"geometry":{"type":"Polygon","coordinates":[[[16.202298,46.852386],[16.534268,47.496171],[16.340584,47.712902],[16.903754,47.714866],[16.979667,48.123497],[17.488473,47.867466],[17.857133,47.758429],[18.696513,47.880954],[18.777025,48.081768],[19.174365,48.111379],[19.661364,48.266615],[19.769471,48.202691],[20.239054,48.327567],[20.473562,48.56285],[20.801294,48.623854],[21.872236,48.319971],[22.085608,48.422264],[22.64082,48.15024],[22. [...]
+{"type":"Feature","properties":{"name":"Indonesia"},"geometry":{"type":"MultiPolygon","coordinates":[[[[120.715609,-10.239581],[120.295014,-10.25865],[118.967808,-9.557969],[119.90031,-9.36134],[120.425756,-9.665921],[120.775502,-9.969675],[120.715609,-10.239581]]],[[[124.43595,-10.140001],[123.579982,-10.359987],[123.459989,-10.239995],[123.550009,-9.900016],[123.980009,-9.290027],[124.968682,-8.89279],[125.07002,-9.089987],[125.08852,-9.393173],[124.43595,-10.140001]]],[[[117.900018,-8 [...]
+{"type":"Feature","properties":{"name":"India"},"geometry":{"type":"Polygon","coordinates":[[[77.837451,35.49401],[78.912269,34.321936],[78.811086,33.506198],[79.208892,32.994395],[79.176129,32.48378],[78.458446,32.618164],[78.738894,31.515906],[79.721367,30.882715],[81.111256,30.183481],[80.476721,29.729865],[80.088425,28.79447],[81.057203,28.416095],[81.999987,27.925479],[83.304249,27.364506],[84.675018,27.234901],[85.251779,26.726198],[86.024393,26.630985],[87.227472,26.397898],[88.06 [...]
+{"type":"Feature","properties":{"name":"Ireland"},"geometry":{"type":"Polygon","coordinates":[[[-6.197885,53.867565],[-6.032985,53.153164],[-6.788857,52.260118],[-8.561617,51.669301],[-9.977086,51.820455],[-9.166283,52.864629],[-9.688525,53.881363],[-8.327987,54.664519],[-7.572168,55.131622],[-7.366031,54.595841],[-7.572168,54.059956],[-6.95373,54.073702],[-6.197885,53.867565]]]},"id":"IRL"},
+{"type":"Feature","properties":{"name":"Iran"},"geometry":{"type":"Polygon","coordinates":[[[53.921598,37.198918],[54.800304,37.392421],[55.511578,37.964117],[56.180375,37.935127],[56.619366,38.121394],[57.330434,38.029229],[58.436154,37.522309],[59.234762,37.412988],[60.377638,36.527383],[61.123071,36.491597],[61.210817,35.650072],[60.803193,34.404102],[60.52843,33.676446],[60.9637,33.528832],[60.536078,32.981269],[60.863655,32.18292],[60.941945,31.548075],[61.699314,31.379506],[61.7812 [...]
+{"type":"Feature","properties":{"name":"Iraq"},"geometry":{"type":"Polygon","coordinates":[[[45.420618,35.977546],[46.07634,35.677383],[46.151788,35.093259],[45.64846,34.748138],[45.416691,33.967798],[46.109362,33.017287],[47.334661,32.469155],[47.849204,31.709176],[47.685286,30.984853],[48.004698,30.985137],[48.014568,30.452457],[48.567971,29.926778],[47.974519,29.975819],[47.302622,30.05907],[46.568713,29.099025],[44.709499,29.178891],[41.889981,31.190009],[40.399994,31.889992],[39.195 [...]
+{"type":"Feature","properties":{"name":"Iceland"},"geometry":{"type":"Polygon","coordinates":[[[-14.508695,66.455892],[-14.739637,65.808748],[-13.609732,65.126671],[-14.909834,64.364082],[-17.794438,63.678749],[-18.656246,63.496383],[-19.972755,63.643635],[-22.762972,63.960179],[-21.778484,64.402116],[-23.955044,64.89113],[-22.184403,65.084968],[-22.227423,65.378594],[-24.326184,65.611189],[-23.650515,66.262519],[-22.134922,66.410469],[-20.576284,65.732112],[-19.056842,66.276601],[-17.79 [...]
+{"type":"Feature","properties":{"name":"Israel"},"geometry":{"type":"Polygon","coordinates":[[[35.719918,32.709192],[35.545665,32.393992],[35.18393,32.532511],[34.974641,31.866582],[35.225892,31.754341],[34.970507,31.616778],[34.927408,31.353435],[35.397561,31.489086],[35.420918,31.100066],[34.922603,29.501326],[34.265433,31.219361],[34.556372,31.548824],[34.488107,31.605539],[34.752587,32.072926],[34.955417,32.827376],[35.098457,33.080539],[35.126053,33.0909],[35.460709,33.08904],[35.55 [...]
+{"type":"Feature","properties":{"name":"Italy"},"geometry":{"type":"MultiPolygon","coordinates":[[[[15.520376,38.231155],[15.160243,37.444046],[15.309898,37.134219],[15.099988,36.619987],[14.335229,36.996631],[13.826733,37.104531],[12.431004,37.61295],[12.570944,38.126381],[13.741156,38.034966],[14.761249,38.143874],[15.520376,38.231155]]],[[[9.210012,41.209991],[9.809975,40.500009],[9.669519,39.177376],[9.214818,39.240473],[8.806936,38.906618],[8.428302,39.171847],[8.388253,40.378311],[ [...]
+{"type":"Feature","properties":{"name":"Jamaica"},"geometry":{"type":"Polygon","coordinates":[[[-77.569601,18.490525],[-76.896619,18.400867],[-76.365359,18.160701],[-76.199659,17.886867],[-76.902561,17.868238],[-77.206341,17.701116],[-77.766023,17.861597],[-78.337719,18.225968],[-78.217727,18.454533],[-77.797365,18.524218],[-77.569601,18.490525]]]},"id":"JAM"},
+{"type":"Feature","properties":{"name":"Jordan"},"geometry":{"type":"Polygon","coordinates":[[[35.545665,32.393992],[35.719918,32.709192],[36.834062,32.312938],[38.792341,33.378686],[39.195468,32.161009],[39.004886,32.010217],[37.002166,31.508413],[37.998849,30.5085],[37.66812,30.338665],[37.503582,30.003776],[36.740528,29.865283],[36.501214,29.505254],[36.068941,29.197495],[34.956037,29.356555],[34.922603,29.501326],[35.420918,31.100066],[35.397561,31.489086],[35.545252,31.782505],[35.5 [...]
+{"type":"Feature","properties":{"name":"Japan"},"geometry":{"type":"MultiPolygon","coordinates":[[[[134.638428,34.149234],[134.766379,33.806335],[134.203416,33.201178],[133.79295,33.521985],[133.280268,33.28957],[133.014858,32.704567],[132.363115,32.989382],[132.371176,33.463642],[132.924373,34.060299],[133.492968,33.944621],[133.904106,34.364931],[134.638428,34.149234]]],[[[140.976388,37.142074],[140.59977,36.343983],[140.774074,35.842877],[140.253279,35.138114],[138.975528,34.6676],[13 [...]
+{"type":"Feature","properties":{"name":"Kazakhstan"},"geometry":{"type":"Polygon","coordinates":[[[70.962315,42.266154],[70.388965,42.081308],[69.070027,41.384244],[68.632483,40.668681],[68.259896,40.662325],[67.985856,41.135991],[66.714047,41.168444],[66.510649,41.987644],[66.023392,41.994646],[66.098012,42.99766],[64.900824,43.728081],[63.185787,43.650075],[62.0133,43.504477],[61.05832,44.405817],[60.239972,44.784037],[58.689989,45.500014],[58.503127,45.586804],[55.928917,44.995858],[5 [...]
+{"type":"Feature","properties":{"name":"Kenya"},"geometry":{"type":"Polygon","coordinates":[[[40.993,-0.85829],[41.58513,-1.68325],[40.88477,-2.08255],[40.63785,-2.49979],[40.26304,-2.57309],[40.12119,-3.27768],[39.80006,-3.68116],[39.60489,-4.34653],[39.20222,-4.67677],[37.7669,-3.67712],[37.69869,-3.09699],[34.07262,-1.05982],[33.903711,-0.95],[33.893569,0.109814],[34.18,0.515],[34.6721,1.17694],[35.03599,1.90584],[34.59607,3.05374],[34.47913,3.5556],[34.005,4.249885],[34.620196,4.8471 [...]
+{"type":"Feature","properties":{"name":"Kyrgyzstan"},"geometry":{"type":"Polygon","coordinates":[[[70.962315,42.266154],[71.186281,42.704293],[71.844638,42.845395],[73.489758,42.500894],[73.645304,43.091272],[74.212866,43.298339],[75.636965,42.8779],[76.000354,42.988022],[77.658392,42.960686],[79.142177,42.856092],[79.643645,42.496683],[80.25999,42.349999],[80.11943,42.123941],[78.543661,41.582243],[78.187197,41.185316],[76.904484,41.066486],[76.526368,40.427946],[75.467828,40.562072],[7 [...]
+{"type":"Feature","properties":{"name":"Cambodia"},"geometry":{"type":"Polygon","coordinates":[[[103.49728,10.632555],[103.09069,11.153661],[102.584932,12.186595],[102.348099,13.394247],[102.988422,14.225721],[104.281418,14.416743],[105.218777,14.273212],[106.043946,13.881091],[106.496373,14.570584],[107.382727,14.202441],[107.614548,13.535531],[107.491403,12.337206],[105.810524,11.567615],[106.24967,10.961812],[105.199915,10.88931],[104.334335,10.486544],[103.49728,10.632555]]]},"id":"KHM"},
+{"type":"Feature","properties":{"name":"South Korea"},"geometry":{"type":"Polygon","coordinates":[[[128.349716,38.612243],[129.21292,37.432392],[129.46045,36.784189],[129.468304,35.632141],[129.091377,35.082484],[128.18585,34.890377],[127.386519,34.475674],[126.485748,34.390046],[126.37392,34.93456],[126.559231,35.684541],[126.117398,36.725485],[126.860143,36.893924],[126.174759,37.749686],[126.237339,37.840378],[126.68372,37.804773],[127.073309,38.256115],[127.780035,38.304536],[128.205 [...]
+{"type":"Feature","properties":{"name":"Kosovo"},"geometry":{"type":"Polygon","coordinates":[[[20.76216,42.05186],[20.71731,41.84711],[20.59023,41.85541],[20.52295,42.21787],[20.28374,42.32025],[20.0707,42.58863],[20.25758,42.81275],[20.49679,42.88469],[20.63508,43.21671],[20.81448,43.27205],[20.95651,43.13094],[21.143395,43.068685],[21.27421,42.90959],[21.43866,42.86255],[21.63302,42.67717],[21.77505,42.6827],[21.66292,42.43922],[21.54332,42.32025],[21.576636,42.245224],[21.3527,42.2068 [...]
+{"type":"Feature","properties":{"name":"Kuwait"},"geometry":{"type":"Polygon","coordinates":[[[47.974519,29.975819],[48.183189,29.534477],[48.093943,29.306299],[48.416094,28.552004],[47.708851,28.526063],[47.459822,29.002519],[46.568713,29.099025],[47.302622,30.05907],[47.974519,29.975819]]]},"id":"KWT"},
+{"type":"Feature","properties":{"name":"Laos"},"geometry":{"type":"Polygon","coordinates":[[[105.218777,14.273212],[105.544338,14.723934],[105.589039,15.570316],[104.779321,16.441865],[104.716947,17.428859],[103.956477,18.240954],[103.200192,18.309632],[102.998706,17.961695],[102.413005,17.932782],[102.113592,18.109102],[101.059548,17.512497],[101.035931,18.408928],[101.282015,19.462585],[100.606294,19.508344],[100.548881,20.109238],[100.115988,20.41785],[100.329101,20.786122],[101.18000 [...]
+{"type":"Feature","properties":{"name":"Lebanon"},"geometry":{"type":"Polygon","coordinates":[[[35.821101,33.277426],[35.552797,33.264275],[35.460709,33.08904],[35.126053,33.0909],[35.482207,33.90545],[35.979592,34.610058],[35.998403,34.644914],[36.448194,34.593935],[36.61175,34.201789],[36.06646,33.824912],[35.821101,33.277426]]]},"id":"LBN"},
+{"type":"Feature","properties":{"name":"Liberia"},"geometry":{"type":"Polygon","coordinates":[[[-7.712159,4.364566],[-7.974107,4.355755],[-9.004794,4.832419],[-9.91342,5.593561],[-10.765384,6.140711],[-11.438779,6.785917],[-11.199802,7.105846],[-11.146704,7.396706],[-10.695595,7.939464],[-10.230094,8.406206],[-10.016567,8.428504],[-9.755342,8.541055],[-9.33728,7.928534],[-9.403348,7.526905],[-9.208786,7.313921],[-8.926065,7.309037],[-8.722124,7.711674],[-8.439298,7.686043],[-8.485446,7.3 [...]
+{"type":"Feature","properties":{"name":"Libya"},"geometry":{"type":"Polygon","coordinates":[[[14.8513,22.86295],[14.143871,22.491289],[13.581425,23.040506],[11.999506,23.471668],[11.560669,24.097909],[10.771364,24.562532],[10.303847,24.379313],[9.948261,24.936954],[9.910693,25.365455],[9.319411,26.094325],[9.716286,26.512206],[9.629056,27.140953],[9.756128,27.688259],[9.683885,28.144174],[9.859998,28.95999],[9.805634,29.424638],[9.48214,30.307556],[9.970017,30.539325],[10.056575,30.96183 [...]
+{"type":"Feature","properties":{"name":"Sri Lanka"},"geometry":{"type":"Polygon","coordinates":[[[81.787959,7.523055],[81.637322,6.481775],[81.21802,6.197141],[80.348357,5.96837],[79.872469,6.763463],[79.695167,8.200843],[80.147801,9.824078],[80.838818,9.268427],[81.304319,8.564206],[81.787959,7.523055]]]},"id":"LKA"},
+{"type":"Feature","properties":{"name":"Lesotho"},"geometry":{"type":"Polygon","coordinates":[[[28.978263,-28.955597],[29.325166,-29.257387],[29.018415,-29.743766],[28.8484,-30.070051],[28.291069,-30.226217],[28.107205,-30.545732],[27.749397,-30.645106],[26.999262,-29.875954],[27.532511,-29.242711],[28.074338,-28.851469],[28.5417,-28.647502],[28.978263,-28.955597]]]},"id":"LSO"},
+{"type":"Feature","properties":{"name":"Lithuania"},"geometry":{"type":"Polygon","coordinates":[[[22.731099,54.327537],[22.651052,54.582741],[22.757764,54.856574],[22.315724,55.015299],[21.268449,55.190482],[21.0558,56.031076],[22.201157,56.337802],[23.878264,56.273671],[24.860684,56.372528],[25.000934,56.164531],[25.533047,56.100297],[26.494331,55.615107],[26.588279,55.167176],[25.768433,54.846963],[25.536354,54.282423],[24.450684,53.905702],[23.484128,53.912498],[23.243987,54.220567],[ [...]
+{"type":"Feature","properties":{"name":"Luxembourg"},"geometry":{"type":"Polygon","coordinates":[[[6.043073,50.128052],[6.242751,49.902226],[6.18632,49.463803],[5.897759,49.442667],[5.674052,49.529484],[5.782417,50.090328],[6.043073,50.128052]]]},"id":"LUX"},
+{"type":"Feature","properties":{"name":"Latvia"},"geometry":{"type":"Polygon","coordinates":[[[21.0558,56.031076],[21.090424,56.783873],[21.581866,57.411871],[22.524341,57.753374],[23.318453,57.006236],[24.12073,57.025693],[24.312863,57.793424],[25.164594,57.970157],[25.60281,57.847529],[26.463532,57.476389],[27.288185,57.474528],[27.770016,57.244258],[27.855282,56.759326],[28.176709,56.16913],[27.10246,55.783314],[26.494331,55.615107],[25.533047,56.100297],[25.000934,56.164531],[24.8606 [...]
+{"type":"Feature","properties":{"name":"Morocco"},"geometry":{"type":"Polygon","coordinates":[[[-5.193863,35.755182],[-4.591006,35.330712],[-3.640057,35.399855],[-2.604306,35.179093],[-2.169914,35.168396],[-1.792986,34.527919],[-1.733455,33.919713],[-1.388049,32.864015],[-1.124551,32.651522],[-1.307899,32.262889],[-2.616605,32.094346],[-3.06898,31.724498],[-3.647498,31.637294],[-3.690441,30.896952],[-4.859646,30.501188],[-5.242129,30.000443],[-6.060632,29.7317],[-7.059228,29.579228],[-8. [...]
+{"type":"Feature","properties":{"name":"Moldova"},"geometry":{"type":"Polygon","coordinates":[[[26.619337,48.220726],[26.857824,48.368211],[27.522537,48.467119],[28.259547,48.155562],[28.670891,48.118149],[29.122698,47.849095],[29.050868,47.510227],[29.415135,47.346645],[29.559674,46.928583],[29.908852,46.674361],[29.83821,46.525326],[30.024659,46.423937],[29.759972,46.349988],[29.170654,46.379262],[29.072107,46.517678],[28.862972,46.437889],[28.933717,46.25883],[28.659987,45.939987],[28 [...]
+{"type":"Feature","properties":{"name":"Madagascar"},"geometry":{"type":"Polygon","coordinates":[[[49.543519,-12.469833],[49.808981,-12.895285],[50.056511,-13.555761],[50.217431,-14.758789],[50.476537,-15.226512],[50.377111,-15.706069],[50.200275,-16.000263],[49.860606,-15.414253],[49.672607,-15.710204],[49.863344,-16.451037],[49.774564,-16.875042],[49.498612,-17.106036],[49.435619,-17.953064],[49.041792,-19.118781],[48.548541,-20.496888],[47.930749,-22.391501],[47.547723,-23.781959],[47 [...]
+{"type":"Feature","properties":{"name":"Mexico"},"geometry":{"type":"Polygon","coordinates":[[[-97.140008,25.869997],[-97.528072,24.992144],[-97.702946,24.272343],[-97.776042,22.93258],[-97.872367,22.444212],[-97.699044,21.898689],[-97.38896,21.411019],[-97.189333,20.635433],[-96.525576,19.890931],[-96.292127,19.320371],[-95.900885,18.828024],[-94.839063,18.562717],[-94.42573,18.144371],[-93.548651,18.423837],[-92.786114,18.524839],[-92.037348,18.704569],[-91.407903,18.876083],[-90.77187 [...]
+{"type":"Feature","properties":{"name":"Macedonia"},"geometry":{"type":"Polygon","coordinates":[[[20.59023,41.85541],[20.71731,41.84711],[20.76216,42.05186],[21.3527,42.2068],[21.576636,42.245224],[21.91708,42.30364],[22.380526,42.32026],[22.881374,41.999297],[22.952377,41.337994],[22.76177,41.3048],[22.597308,41.130487],[22.055378,41.149866],[21.674161,40.931275],[21.02004,40.842727],[20.60518,41.08622],[20.46315,41.51509],[20.59023,41.85541]]]},"id":"MKD"},
+{"type":"Feature","properties":{"name":"Mali"},"geometry":{"type":"Polygon","coordinates":[[[-12.17075,14.616834],[-11.834208,14.799097],[-11.666078,15.388208],[-11.349095,15.411256],[-10.650791,15.132746],[-10.086846,15.330486],[-9.700255,15.264107],[-9.550238,15.486497],[-5.537744,15.50169],[-5.315277,16.201854],[-5.488523,16.325102],[-5.971129,20.640833],[-6.453787,24.956591],[-4.923337,24.974574],[-1.550055,22.792666],[1.823228,20.610809],[2.060991,20.142233],[2.683588,19.85623],[3.1 [...]
+{"type":"Feature","properties":{"name":"Myanmar"},"geometry":{"type":"Polygon","coordinates":[[[99.543309,20.186598],[98.959676,19.752981],[98.253724,19.708203],[97.797783,18.62708],[97.375896,18.445438],[97.859123,17.567946],[98.493761,16.837836],[98.903348,16.177824],[98.537376,15.308497],[98.192074,15.123703],[98.430819,14.622028],[99.097755,13.827503],[99.212012,13.269294],[99.196354,12.804748],[99.587286,11.892763],[99.038121,10.960546],[98.553551,9.93296],[98.457174,10.675266],[98. [...]
+{"type":"Feature","properties":{"name":"Montenegro"},"geometry":{"type":"Polygon","coordinates":[[[19.801613,42.500093],[19.738051,42.688247],[19.30449,42.19574],[19.37177,41.87755],[19.16246,41.95502],[18.88214,42.28151],[18.45,42.48],[18.56,42.65],[18.70648,43.20011],[19.03165,43.43253],[19.21852,43.52384],[19.48389,43.35229],[19.63,43.21378],[19.95857,43.10604],[20.3398,42.89852],[20.25758,42.81275],[20.0707,42.58863],[19.801613,42.500093]]]},"id":"MNE"},
+{"type":"Feature","properties":{"name":"Mongolia"},"geometry":{"type":"Polygon","coordinates":[[[87.751264,49.297198],[88.805567,49.470521],[90.713667,50.331812],[92.234712,50.802171],[93.104219,50.49529],[94.147566,50.480537],[94.815949,50.013433],[95.814028,49.977467],[97.259728,49.726061],[98.231762,50.422401],[97.82574,51.010995],[98.861491,52.047366],[99.981732,51.634006],[100.88948,51.516856],[102.065223,51.259921],[102.255909,50.510561],[103.676545,50.089966],[104.621552,50.275329 [...]
+{"type":"Feature","properties":{"name":"Mozambique"},"geometry":{"type":"Polygon","coordinates":[[[34.559989,-11.52002],[35.312398,-11.439146],[36.514082,-11.720938],[36.775151,-11.594537],[37.471284,-11.568751],[37.827645,-11.268769],[38.427557,-11.285202],[39.52103,-10.896854],[40.316589,-10.317096],[40.478387,-10.765441],[40.437253,-11.761711],[40.560811,-12.639177],[40.59962,-14.201975],[40.775475,-14.691764],[40.477251,-15.406294],[40.089264,-16.100774],[39.452559,-16.720891],[38.53 [...]
+{"type":"Feature","properties":{"name":"Mauritania"},"geometry":{"type":"Polygon","coordinates":[[[-12.17075,14.616834],[-12.830658,15.303692],[-13.435738,16.039383],[-14.099521,16.304302],[-14.577348,16.598264],[-15.135737,16.587282],[-15.623666,16.369337],[-16.12069,16.455663],[-16.463098,16.135036],[-16.549708,16.673892],[-16.270552,17.166963],[-16.146347,18.108482],[-16.256883,19.096716],[-16.377651,19.593817],[-16.277838,20.092521],[-16.536324,20.567866],[-17.063423,20.999752],[-16. [...]
+{"type":"Feature","properties":{"name":"Malawi"},"geometry":{"type":"Polygon","coordinates":[[[34.559989,-11.52002],[34.280006,-12.280025],[34.559989,-13.579998],[34.907151,-13.565425],[35.267956,-13.887834],[35.686845,-14.611046],[35.771905,-15.896859],[35.339063,-16.10744],[35.03381,-16.8013],[34.381292,-16.18356],[34.307291,-15.478641],[34.517666,-15.013709],[34.459633,-14.61301],[34.064825,-14.35995],[33.7897,-14.451831],[33.214025,-13.97186],[32.688165,-13.712858],[32.991764,-12.783 [...]
+{"type":"Feature","properties":{"name":"Malaysia"},"geometry":{"type":"MultiPolygon","coordinates":[[[[101.075516,6.204867],[101.154219,5.691384],[101.814282,5.810808],[102.141187,6.221636],[102.371147,6.128205],[102.961705,5.524495],[103.381215,4.855001],[103.438575,4.181606],[103.332122,3.726698],[103.429429,3.382869],[103.502448,2.791019],[103.854674,2.515454],[104.247932,1.631141],[104.228811,1.293048],[103.519707,1.226334],[102.573615,1.967115],[101.390638,2.760814],[101.27354,3.270 [...]
+{"type":"Feature","properties":{"name":"Namibia"},"geometry":{"type":"Polygon","coordinates":[[[16.344977,-28.576705],[15.601818,-27.821247],[15.210472,-27.090956],[14.989711,-26.117372],[14.743214,-25.39292],[14.408144,-23.853014],[14.385717,-22.656653],[14.257714,-22.111208],[13.868642,-21.699037],[13.352498,-20.872834],[12.826845,-19.673166],[12.608564,-19.045349],[11.794919,-18.069129],[11.734199,-17.301889],[12.215461,-17.111668],[12.814081,-16.941343],[13.462362,-16.971212],[14.058 [...]
+{"type":"Feature","properties":{"name":"New Caledonia"},"geometry":{"type":"Polygon","coordinates":[[[165.77999,-21.080005],[166.599991,-21.700019],[167.120011,-22.159991],[166.740035,-22.399976],[166.189732,-22.129708],[165.474375,-21.679607],[164.829815,-21.14982],[164.167995,-20.444747],[164.029606,-20.105646],[164.459967,-20.120012],[165.020036,-20.459991],[165.460009,-20.800022],[165.77999,-21.080005]]]},"id":"NCL"},
+{"type":"Feature","properties":{"name":"Niger"},"geometry":{"type":"Polygon","coordinates":[[[2.154474,11.94015],[2.177108,12.625018],[1.024103,12.851826],[0.993046,13.33575],[0.429928,13.988733],[0.295646,14.444235],[0.374892,14.928908],[1.015783,14.968182],[1.385528,15.323561],[2.749993,15.409525],[3.638259,15.56812],[3.723422,16.184284],[4.27021,16.852227],[4.267419,19.155265],[5.677566,19.601207],[8.572893,21.565661],[11.999506,23.471668],[13.581425,23.040506],[14.143871,22.491289],[ [...]
+{"type":"Feature","properties":{"name":"Nigeria"},"geometry":{"type":"Polygon","coordinates":[[[8.500288,4.771983],[7.462108,4.412108],[7.082596,4.464689],[6.698072,4.240594],[5.898173,4.262453],[5.362805,4.887971],[5.033574,5.611802],[4.325607,6.270651],[3.57418,6.2583],[2.691702,6.258817],[2.749063,7.870734],[2.723793,8.506845],[2.912308,9.137608],[3.220352,9.444153],[3.705438,10.06321],[3.60007,10.332186],[3.797112,10.734746],[3.572216,11.327939],[3.61118,11.660167],[3.680634,12.55290 [...]
+{"type":"Feature","properties":{"name":"Nicaragua"},"geometry":{"type":"Polygon","coordinates":[[[-85.71254,11.088445],[-86.058488,11.403439],[-86.52585,11.806877],[-86.745992,12.143962],[-87.167516,12.458258],[-87.668493,12.90991],[-87.557467,13.064552],[-87.392386,12.914018],[-87.316654,12.984686],[-87.005769,13.025794],[-86.880557,13.254204],[-86.733822,13.263093],[-86.755087,13.754845],[-86.520708,13.778487],[-86.312142,13.771356],[-86.096264,14.038187],[-85.801295,13.836055],[-85.69 [...]
+{"type":"Feature","properties":{"name":"Netherlands"},"geometry":{"type":"Polygon","coordinates":[[[6.074183,53.510403],[6.90514,53.482162],[7.092053,53.144043],[6.84287,52.22844],[6.589397,51.852029],[5.988658,51.851616],[6.156658,50.803721],[5.606976,51.037298],[4.973991,51.475024],[4.047071,51.267259],[3.314971,51.345755],[3.830289,51.620545],[4.705997,53.091798],[6.074183,53.510403]]]},"id":"NLD"},
+{"type":"Feature","properties":{"name":"Norway"},"geometry":{"type":"MultiPolygon","coordinates":[[[[28.165547,71.185474],[31.293418,70.453788],[30.005435,70.186259],[31.101079,69.55808],[29.399581,69.156916],[28.59193,69.064777],[29.015573,69.766491],[27.732292,70.164193],[26.179622,69.825299],[25.689213,69.092114],[24.735679,68.649557],[23.66205,68.891247],[22.356238,68.841741],[21.244936,69.370443],[20.645593,69.106247],[20.025269,69.065139],[19.87856,68.407194],[17.993868,68.567391], [...]
+{"type":"Feature","properties":{"name":"Nepal"},"geometry":{"type":"Polygon","coordinates":[[[88.120441,27.876542],[88.043133,27.445819],[88.174804,26.810405],[88.060238,26.414615],[87.227472,26.397898],[86.024393,26.630985],[85.251779,26.726198],[84.675018,27.234901],[83.304249,27.364506],[81.999987,27.925479],[81.057203,28.416095],[80.088425,28.79447],[80.476721,29.729865],[81.111256,30.183481],[81.525804,30.422717],[82.327513,30.115268],[83.337115,29.463732],[83.898993,29.320226],[84. [...]
+{"type":"Feature","properties":{"name":"New Zealand"},"geometry":{"type":"MultiPolygon","coordinates":[[[[173.020375,-40.919052],[173.247234,-41.331999],[173.958405,-40.926701],[174.247587,-41.349155],[174.248517,-41.770008],[173.876447,-42.233184],[173.22274,-42.970038],[172.711246,-43.372288],[173.080113,-43.853344],[172.308584,-43.865694],[171.452925,-44.242519],[171.185138,-44.897104],[170.616697,-45.908929],[169.831422,-46.355775],[169.332331,-46.641235],[168.411354,-46.619945],[167 [...]
+{"type":"Feature","properties":{"name":"Oman"},"geometry":{"type":"MultiPolygon","coordinates":[[[[58.861141,21.114035],[58.487986,20.428986],[58.034318,20.481437],[57.826373,20.243002],[57.665762,19.736005],[57.7887,19.06757],[57.694391,18.94471],[57.234264,18.947991],[56.609651,18.574267],[56.512189,18.087113],[56.283521,17.876067],[55.661492,17.884128],[55.269939,17.632309],[55.2749,17.228354],[54.791002,16.950697],[54.239253,17.044981],[53.570508,16.707663],[53.108573,16.651051],[52. [...]
+{"type":"Feature","properties":{"name":"Pakistan"},"geometry":{"type":"Polygon","coordinates":[[[75.158028,37.133031],[75.896897,36.666806],[76.192848,35.898403],[77.837451,35.49401],[76.871722,34.653544],[75.757061,34.504923],[74.240203,34.748887],[73.749948,34.317699],[74.104294,33.441473],[74.451559,32.7649],[75.258642,32.271105],[74.405929,31.692639],[74.42138,30.979815],[73.450638,29.976413],[72.823752,28.961592],[71.777666,27.91318],[70.616496,27.989196],[69.514393,26.940966],[70.1 [...]
+{"type":"Feature","properties":{"name":"Panama"},"geometry":{"type":"Polygon","coordinates":[[[-77.881571,7.223771],[-78.214936,7.512255],[-78.429161,8.052041],[-78.182096,8.319182],[-78.435465,8.387705],[-78.622121,8.718124],[-79.120307,8.996092],[-79.557877,8.932375],[-79.760578,8.584515],[-80.164481,8.333316],[-80.382659,8.298409],[-80.480689,8.090308],[-80.00369,7.547524],[-80.276671,7.419754],[-80.421158,7.271572],[-80.886401,7.220541],[-81.059543,7.817921],[-81.189716,7.647906],[-8 [...]
+{"type":"Feature","properties":{"name":"Peru"},"geometry":{"type":"Polygon","coordinates":[[[-69.590424,-17.580012],[-69.858444,-18.092694],[-70.372572,-18.347975],[-71.37525,-17.773799],[-71.462041,-17.363488],[-73.44453,-16.359363],[-75.237883,-15.265683],[-76.009205,-14.649286],[-76.423469,-13.823187],[-76.259242,-13.535039],[-77.106192,-12.222716],[-78.092153,-10.377712],[-79.036953,-8.386568],[-79.44592,-7.930833],[-79.760578,-7.194341],[-80.537482,-6.541668],[-81.249996,-6.136834], [...]
+{"type":"Feature","properties":{"name":"Philippines"},"geometry":{"type":"MultiPolygon","coordinates":[[[[126.376814,8.414706],[126.478513,7.750354],[126.537424,7.189381],[126.196773,6.274294],[125.831421,7.293715],[125.363852,6.786485],[125.683161,6.049657],[125.396512,5.581003],[124.219788,6.161355],[123.93872,6.885136],[124.243662,7.36061],[123.610212,7.833527],[123.296071,7.418876],[122.825506,7.457375],[122.085499,6.899424],[121.919928,7.192119],[122.312359,8.034962],[122.942398,8.3 [...]
+{"type":"Feature","properties":{"name":"Papua New Guinea"},"geometry":{"type":"MultiPolygon","coordinates":[[[[155.880026,-6.819997],[155.599991,-6.919991],[155.166994,-6.535931],[154.729192,-5.900828],[154.514114,-5.139118],[154.652504,-5.042431],[154.759991,-5.339984],[155.062918,-5.566792],[155.547746,-6.200655],[156.019965,-6.540014],[155.880026,-6.819997]]],[[[151.982796,-5.478063],[151.459107,-5.56028],[151.30139,-5.840728],[150.754447,-6.083763],[150.241197,-6.317754],[149.709963, [...]
+{"type":"Feature","properties":{"name":"Poland"},"geometry":{"type":"Polygon","coordinates":[[[15.016996,51.106674],[14.607098,51.745188],[14.685026,52.089947],[14.4376,52.62485],[14.074521,52.981263],[14.353315,53.248171],[14.119686,53.757029],[14.8029,54.050706],[16.363477,54.513159],[17.622832,54.851536],[18.620859,54.682606],[18.696255,54.438719],[19.66064,54.426084],[20.892245,54.312525],[22.731099,54.327537],[23.243987,54.220567],[23.484128,53.912498],[23.527536,53.470122],[23.8049 [...]
+{"type":"Feature","properties":{"name":"Puerto Rico"},"geometry":{"type":"Polygon","coordinates":[[[-66.282434,18.514762],[-65.771303,18.426679],[-65.591004,18.228035],[-65.847164,17.975906],[-66.599934,17.981823],[-67.184162,17.946553],[-67.242428,18.37446],[-67.100679,18.520601],[-66.282434,18.514762]]]},"id":"PRI"},
+{"type":"Feature","properties":{"name":"North Korea"},"geometry":{"type":"Polygon","coordinates":[[[130.640016,42.395009],[130.780007,42.220007],[130.400031,42.280004],[129.965949,41.941368],[129.667362,41.601104],[129.705189,40.882828],[129.188115,40.661808],[129.0104,40.485436],[128.633368,40.189847],[127.967414,40.025413],[127.533436,39.75685],[127.50212,39.323931],[127.385434,39.213472],[127.783343,39.050898],[128.349716,38.612243],[128.205746,38.370397],[127.780035,38.304536],[127.0 [...]
+{"type":"Feature","properties":{"name":"Portugal"},"geometry":{"type":"Polygon","coordinates":[[[-9.034818,41.880571],[-8.671946,42.134689],[-8.263857,42.280469],[-8.013175,41.790886],[-7.422513,41.792075],[-7.251309,41.918346],[-6.668606,41.883387],[-6.389088,41.381815],[-6.851127,41.111083],[-6.86402,40.330872],[-7.026413,40.184524],[-7.066592,39.711892],[-7.498632,39.629571],[-7.098037,39.030073],[-7.374092,38.373059],[-7.029281,38.075764],[-7.166508,37.803894],[-7.537105,37.428904],[ [...]
+{"type":"Feature","properties":{"name":"Paraguay"},"geometry":{"type":"Polygon","coordinates":[[[-62.685057,-22.249029],[-62.291179,-21.051635],[-62.265961,-20.513735],[-61.786326,-19.633737],[-60.043565,-19.342747],[-59.115042,-19.356906],[-58.183471,-19.868399],[-58.166392,-20.176701],[-57.870674,-20.732688],[-57.937156,-22.090176],[-56.88151,-22.282154],[-56.473317,-22.0863],[-55.797958,-22.35693],[-55.610683,-22.655619],[-55.517639,-23.571998],[-55.400747,-23.956935],[-55.027902,-24. [...]
+{"type":"Feature","properties":{"name":"Qatar"},"geometry":{"type":"Polygon","coordinates":[[[50.810108,24.754743],[50.743911,25.482424],[51.013352,26.006992],[51.286462,26.114582],[51.589079,25.801113],[51.6067,25.21567],[51.389608,24.627386],[51.112415,24.556331],[50.810108,24.754743]]]},"id":"QAT"},
+{"type":"Feature","properties":{"name":"Romania"},"geometry":{"type":"Polygon","coordinates":[[[22.710531,47.882194],[23.142236,48.096341],[23.760958,47.985598],[24.402056,47.981878],[24.866317,47.737526],[25.207743,47.891056],[25.945941,47.987149],[26.19745,48.220881],[26.619337,48.220726],[26.924176,48.123264],[27.233873,47.826771],[27.551166,47.405117],[28.12803,46.810476],[28.160018,46.371563],[28.054443,45.944586],[28.233554,45.488283],[28.679779,45.304031],[29.149725,45.464925],[29 [...]
+{"type":"Feature","properties":{"name":"Russia"},"geometry":{"type":"MultiPolygon","coordinates":[[[[143.648007,50.7476],[144.654148,48.976391],[143.173928,49.306551],[142.558668,47.861575],[143.533492,46.836728],[143.505277,46.137908],[142.747701,46.740765],[142.09203,45.966755],[141.906925,46.805929],[142.018443,47.780133],[141.904445,48.859189],[142.1358,49.615163],[142.179983,50.952342],[141.594076,51.935435],[141.682546,53.301966],[142.606934,53.762145],[142.209749,54.225476],[142.6 [...]
+{"type":"Feature","properties":{"name":"Rwanda"},"geometry":{"type":"Polygon","coordinates":[[[30.419105,-1.134659],[30.816135,-1.698914],[30.758309,-2.28725],[30.469696,-2.413858],[29.938359,-2.348487],[29.632176,-2.917858],[29.024926,-2.839258],[29.117479,-2.292211],[29.254835,-2.21511],[29.291887,-1.620056],[29.579466,-1.341313],[29.821519,-1.443322],[30.419105,-1.134659]]]},"id":"RWA"},
+{"type":"Feature","properties":{"name":"Western Sahara"},"geometry":{"type":"Polygon","coordinates":[[[-8.794884,27.120696],[-8.817828,27.656426],[-8.66559,27.656426],[-8.665124,27.589479],[-8.6844,27.395744],[-8.687294,25.881056],[-11.969419,25.933353],[-11.937224,23.374594],[-12.874222,23.284832],[-13.118754,22.77122],[-12.929102,21.327071],[-16.845194,21.333323],[-17.063423,20.999752],[-17.020428,21.42231],[-17.002962,21.420734],[-14.750955,21.5006],[-14.630833,21.86094],[-14.221168,2 [...]
+{"type":"Feature","properties":{"name":"Saudi Arabia"},"geometry":{"type":"Polygon","coordinates":[[[42.779332,16.347891],[42.649573,16.774635],[42.347989,17.075806],[42.270888,17.474722],[41.754382,17.833046],[41.221391,18.6716],[40.939341,19.486485],[40.247652,20.174635],[39.801685,20.338862],[39.139399,21.291905],[39.023696,21.986875],[39.066329,22.579656],[38.492772,23.688451],[38.02386,24.078686],[37.483635,24.285495],[37.154818,24.858483],[37.209491,25.084542],[36.931627,25.602959] [...]
+{"type":"Feature","properties":{"name":"Sudan"},"geometry":{"type":"Polygon","coordinates":[[[33.963393,9.464285],[33.824963,9.484061],[33.842131,9.981915],[33.721959,10.325262],[33.206938,10.720112],[33.086766,11.441141],[33.206938,12.179338],[32.743419,12.248008],[32.67475,12.024832],[32.073892,11.97333],[32.314235,11.681484],[32.400072,11.080626],[31.850716,10.531271],[31.352862,9.810241],[30.837841,9.707237],[29.996639,10.290927],[29.618957,10.084919],[29.515953,9.793074],[29.000932, [...]
+{"type":"Feature","properties":{"name":"South Sudan"},"geometry":{"type":"Polygon","coordinates":[[[33.963393,9.464285],[33.97498,8.68456],[33.8255,8.37916],[33.2948,8.35458],[32.95418,7.78497],[33.56829,7.71334],[34.0751,7.22595],[34.25032,6.82607],[34.70702,6.59422],[35.298007,5.506],[34.620196,4.847123],[34.005,4.249885],[33.39,3.79],[32.68642,3.79232],[31.88145,3.55827],[31.24556,3.7819],[30.83385,3.50917],[29.95349,4.1737],[29.715995,4.600805],[29.159078,4.389267],[28.696678,4.45507 [...]
+{"type":"Feature","properties":{"name":"Senegal"},"geometry":{"type":"Polygon","coordinates":[[[-16.713729,13.594959],[-17.126107,14.373516],[-17.625043,14.729541],[-17.185173,14.919477],[-16.700706,15.621527],[-16.463098,16.135036],[-16.12069,16.455663],[-15.623666,16.369337],[-15.135737,16.587282],[-14.577348,16.598264],[-14.099521,16.304302],[-13.435738,16.039383],[-12.830658,15.303692],[-12.17075,14.616834],[-12.124887,13.994727],[-11.927716,13.422075],[-11.553398,13.141214],[-11.467 [...]
+{"type":"Feature","properties":{"name":"Solomon Islands"},"geometry":{"type":"MultiPolygon","coordinates":[[[[162.119025,-10.482719],[162.398646,-10.826367],[161.700032,-10.820011],[161.319797,-10.204751],[161.917383,-10.446701],[162.119025,-10.482719]]],[[[160.852229,-9.872937],[160.462588,-9.89521],[159.849447,-9.794027],[159.640003,-9.63998],[159.702945,-9.24295],[160.362956,-9.400304],[160.688518,-9.610162],[160.852229,-9.872937]]],[[[161.679982,-9.599982],[161.529397,-9.784312],[160 [...]
+{"type":"Feature","properties":{"name":"Sierra Leone"},"geometry":{"type":"Polygon","coordinates":[[[-11.438779,6.785917],[-11.708195,6.860098],[-12.428099,7.262942],[-12.949049,7.798646],[-13.124025,8.163946],[-13.24655,8.903049],[-12.711958,9.342712],[-12.596719,9.620188],[-12.425929,9.835834],[-12.150338,9.858572],[-11.917277,10.046984],[-11.117481,10.045873],[-10.839152,9.688246],[-10.622395,9.26791],[-10.65477,8.977178],[-10.494315,8.715541],[-10.505477,8.348896],[-10.230094,8.40620 [...]
+{"type":"Feature","properties":{"name":"El Salvador"},"geometry":{"type":"Polygon","coordinates":[[[-87.793111,13.38448],[-87.904112,13.149017],[-88.483302,13.163951],[-88.843228,13.259734],[-89.256743,13.458533],[-89.812394,13.520622],[-90.095555,13.735338],[-90.064678,13.88197],[-89.721934,14.134228],[-89.534219,14.244816],[-89.587343,14.362586],[-89.353326,14.424133],[-89.058512,14.340029],[-88.843073,14.140507],[-88.541231,13.980155],[-88.503998,13.845486],[-88.065343,13.964626],[-87 [...]
+{"type":"Feature","properties":{"name":"Somaliland"},"geometry":{"type":"Polygon","coordinates":[[[48.93813,9.451749],[48.486736,8.837626],[47.78942,8.003],[46.948328,7.996877],[43.67875,9.18358],[43.296975,9.540477],[42.92812,10.02194],[42.55876,10.57258],[42.776852,10.926879],[43.145305,11.46204],[43.47066,11.27771],[43.666668,10.864169],[44.117804,10.445538],[44.614259,10.442205],[45.556941,10.698029],[46.645401,10.816549],[47.525658,11.127228],[48.021596,11.193064],[48.378784,11.3754 [...]
+{"type":"Feature","properties":{"name":"Somalia"},"geometry":{"type":"Polygon","coordinates":[[[49.72862,11.5789],[50.25878,11.67957],[50.73202,12.0219],[51.1112,12.02464],[51.13387,11.74815],[51.04153,11.16651],[51.04531,10.6409],[50.83418,10.27972],[50.55239,9.19874],[50.07092,8.08173],[49.4527,6.80466],[48.59455,5.33911],[47.74079,4.2194],[46.56476,2.85529],[45.56399,2.04576],[44.06815,1.05283],[43.13597,0.2922],[42.04157,-0.91916],[41.81095,-1.44647],[41.58513,-1.68325],[40.993,-0.85 [...]
+{"type":"Feature","properties":{"name":"Republic of Serbia"},"geometry":{"type":"Polygon","coordinates":[[[20.874313,45.416375],[21.483526,45.18117],[21.562023,44.768947],[22.145088,44.478422],[22.459022,44.702517],[22.705726,44.578003],[22.474008,44.409228],[22.65715,44.234923],[22.410446,44.008063],[22.500157,43.642814],[22.986019,43.211161],[22.604801,42.898519],[22.436595,42.580321],[22.545012,42.461362],[22.380526,42.32026],[21.91708,42.30364],[21.576636,42.245224],[21.54332,42.3202 [...]
+{"type":"Feature","properties":{"name":"Suriname"},"geometry":{"type":"Polygon","coordinates":[[[-57.147436,5.97315],[-55.949318,5.772878],[-55.84178,5.953125],[-55.03325,6.025291],[-53.958045,5.756548],[-54.478633,4.896756],[-54.399542,4.212611],[-54.006931,3.620038],[-54.181726,3.18978],[-54.269705,2.732392],[-54.524754,2.311849],[-55.097587,2.523748],[-55.569755,2.421506],[-55.973322,2.510364],[-56.073342,2.220795],[-55.9056,2.021996],[-55.995698,1.817667],[-56.539386,1.899523],[-57.1 [...]
+{"type":"Feature","properties":{"name":"Slovakia"},"geometry":{"type":"Polygon","coordinates":[[[18.853144,49.49623],[18.909575,49.435846],[19.320713,49.571574],[19.825023,49.217125],[20.415839,49.431453],[20.887955,49.328772],[21.607808,49.470107],[22.558138,49.085738],[22.280842,48.825392],[22.085608,48.422264],[21.872236,48.319971],[20.801294,48.623854],[20.473562,48.56285],[20.239054,48.327567],[19.769471,48.202691],[19.661364,48.266615],[19.174365,48.111379],[18.777025,48.081768],[1 [...]
+{"type":"Feature","properties":{"name":"Slovenia"},"geometry":{"type":"Polygon","coordinates":[[[13.806475,46.509306],[14.632472,46.431817],[15.137092,46.658703],[16.011664,46.683611],[16.202298,46.852386],[16.370505,46.841327],[16.564808,46.503751],[15.768733,46.238108],[15.67153,45.834154],[15.323954,45.731783],[15.327675,45.452316],[14.935244,45.471695],[14.595109,45.634941],[14.411968,45.466166],[13.71506,45.500324],[13.93763,45.591016],[13.69811,46.016778],[13.806475,46.509306]]]}," [...]
+{"type":"Feature","properties":{"name":"Sweden"},"geometry":{"type":"Polygon","coordinates":[[[22.183173,65.723741],[21.213517,65.026005],[21.369631,64.413588],[19.778876,63.609554],[17.847779,62.7494],[17.119555,61.341166],[17.831346,60.636583],[18.787722,60.081914],[17.869225,58.953766],[16.829185,58.719827],[16.44771,57.041118],[15.879786,56.104302],[14.666681,56.200885],[14.100721,55.407781],[12.942911,55.361737],[12.625101,56.30708],[11.787942,57.441817],[11.027369,58.856149],[11.46 [...]
+{"type":"Feature","properties":{"name":"Swaziland"},"geometry":{"type":"Polygon","coordinates":[[[32.071665,-26.73382],[31.86806,-27.177927],[31.282773,-27.285879],[30.685962,-26.743845],[30.676609,-26.398078],[30.949667,-26.022649],[31.04408,-25.731452],[31.333158,-25.660191],[31.837778,-25.843332],[31.985779,-26.29178],[32.071665,-26.73382]]]},"id":"SWZ"},
+{"type":"Feature","properties":{"name":"Syria"},"geometry":{"type":"Polygon","coordinates":[[[38.792341,33.378686],[36.834062,32.312938],[35.719918,32.709192],[35.700798,32.716014],[35.836397,32.868123],[35.821101,33.277426],[36.06646,33.824912],[36.61175,34.201789],[36.448194,34.593935],[35.998403,34.644914],[35.905023,35.410009],[36.149763,35.821535],[36.41755,36.040617],[36.685389,36.259699],[36.739494,36.81752],[37.066761,36.623036],[38.167727,36.90121],[38.699891,36.712927],[39.5225 [...]
+{"type":"Feature","properties":{"name":"Chad"},"geometry":{"type":"Polygon","coordinates":[[[14.495787,12.859396],[14.595781,13.330427],[13.954477,13.353449],[13.956699,13.996691],[13.540394,14.367134],[13.97217,15.68437],[15.247731,16.627306],[15.300441,17.92795],[15.685741,19.95718],[15.903247,20.387619],[15.487148,20.730415],[15.47106,21.04845],[15.096888,21.308519],[14.8513,22.86295],[15.86085,23.40972],[19.84926,21.49509],[23.83766,19.58047],[23.88689,15.61084],[23.02459,15.68072],[ [...]
+{"type":"Feature","properties":{"name":"Togo"},"geometry":{"type":"Polygon","coordinates":[[[1.865241,6.142158],[1.060122,5.928837],[0.836931,6.279979],[0.570384,6.914359],[0.490957,7.411744],[0.712029,8.312465],[0.461192,8.677223],[0.365901,9.465004],[0.36758,10.191213],[-0.049785,10.706918],[0.023803,11.018682],[0.899563,10.997339],[0.772336,10.470808],[1.077795,10.175607],[1.425061,9.825395],[1.463043,9.334624],[1.664478,9.12859],[1.618951,6.832038],[1.865241,6.142158]]]},"id":"TGO"},
+{"type":"Feature","properties":{"name":"Thailand"},"geometry":{"type":"Polygon","coordinates":[[[102.584932,12.186595],[101.687158,12.64574],[100.83181,12.627085],[100.978467,13.412722],[100.097797,13.406856],[100.018733,12.307001],[99.478921,10.846367],[99.153772,9.963061],[99.222399,9.239255],[99.873832,9.207862],[100.279647,8.295153],[100.459274,7.429573],[101.017328,6.856869],[101.623079,6.740622],[102.141187,6.221636],[101.814282,5.810808],[101.154219,5.691384],[101.075516,6.204867] [...]
+{"type":"Feature","properties":{"name":"Tajikistan"},"geometry":{"type":"Polygon","coordinates":[[[71.014198,40.244366],[70.648019,39.935754],[69.55961,40.103211],[69.464887,39.526683],[70.549162,39.604198],[71.784694,39.279463],[73.675379,39.431237],[73.928852,38.505815],[74.257514,38.606507],[74.864816,38.378846],[74.829986,37.990007],[74.980002,37.41999],[73.948696,37.421566],[73.260056,37.495257],[72.63689,37.047558],[72.193041,36.948288],[71.844638,36.738171],[71.448693,37.065645],[ [...]
+{"type":"Feature","properties":{"name":"Turkmenistan"},"geometry":{"type":"Polygon","coordinates":[[[61.210817,35.650072],[61.123071,36.491597],[60.377638,36.527383],[59.234762,37.412988],[58.436154,37.522309],[57.330434,38.029229],[56.619366,38.121394],[56.180375,37.935127],[55.511578,37.964117],[54.800304,37.392421],[53.921598,37.198918],[53.735511,37.906136],[53.880929,38.952093],[53.101028,39.290574],[53.357808,39.975286],[52.693973,40.033629],[52.915251,40.876523],[53.858139,40.6310 [...]
+{"type":"Feature","properties":{"name":"East Timor"},"geometry":{"type":"Polygon","coordinates":[[[124.968682,-8.89279],[125.086246,-8.656887],[125.947072,-8.432095],[126.644704,-8.398247],[126.957243,-8.273345],[127.335928,-8.397317],[126.967992,-8.668256],[125.925885,-9.106007],[125.08852,-9.393173],[125.07002,-9.089987],[124.968682,-8.89279]]]},"id":"TLS"},
+{"type":"Feature","properties":{"name":"Trinidad and Tobago"},"geometry":{"type":"Polygon","coordinates":[[[-61.68,10.76],[-61.105,10.89],[-60.895,10.855],[-60.935,10.11],[-61.77,10],[-61.95,10.09],[-61.66,10.365],[-61.68,10.76]]]},"id":"TTO"},
+{"type":"Feature","properties":{"name":"Tunisia"},"geometry":{"type":"Polygon","coordinates":[[[9.48214,30.307556],[9.055603,32.102692],[8.439103,32.506285],[8.430473,32.748337],[7.612642,33.344115],[7.524482,34.097376],[8.140981,34.655146],[8.376368,35.479876],[8.217824,36.433177],[8.420964,36.946427],[9.509994,37.349994],[10.210002,37.230002],[10.18065,36.724038],[11.028867,37.092103],[11.100026,36.899996],[10.600005,36.41],[10.593287,35.947444],[10.939519,35.698984],[10.807847,34.8335 [...]
+{"type":"Feature","properties":{"name":"Turkey"},"geometry":{"type":"MultiPolygon","coordinates":[[[[36.913127,41.335358],[38.347665,40.948586],[39.512607,41.102763],[40.373433,41.013673],[41.554084,41.535656],[42.619549,41.583173],[43.582746,41.092143],[43.752658,40.740201],[43.656436,40.253564],[44.400009,40.005],[44.79399,39.713003],[44.109225,39.428136],[44.421403,38.281281],[44.225756,37.971584],[44.772699,37.170445],[44.293452,37.001514],[43.942259,37.256228],[42.779126,37.385264], [...]
+{"type":"Feature","properties":{"name":"Taiwan"},"geometry":{"type":"Polygon","coordinates":[[[121.777818,24.394274],[121.175632,22.790857],[120.74708,21.970571],[120.220083,22.814861],[120.106189,23.556263],[120.69468,24.538451],[121.495044,25.295459],[121.951244,24.997596],[121.777818,24.394274]]]},"id":"TWN"},
+{"type":"Feature","properties":{"name":"United Republic of Tanzania"},"geometry":{"type":"Polygon","coordinates":[[[33.903711,-0.95],[34.07262,-1.05982],[37.69869,-3.09699],[37.7669,-3.67712],[39.20222,-4.67677],[38.74054,-5.90895],[38.79977,-6.47566],[39.44,-6.84],[39.47,-7.1],[39.19469,-7.7039],[39.25203,-8.00781],[39.18652,-8.48551],[39.53574,-9.11237],[39.9496,-10.0984],[40.31659,-10.3171],[39.521,-10.89688],[38.427557,-11.285202],[37.82764,-11.26879],[37.47129,-11.56876],[36.775151, [...]
+{"type":"Feature","properties":{"name":"Uganda"},"geometry":{"type":"Polygon","coordinates":[[[31.86617,-1.02736],[30.76986,-1.01455],[30.419105,-1.134659],[29.821519,-1.443322],[29.579466,-1.341313],[29.587838,-0.587406],[29.8195,-0.2053],[29.875779,0.59738],[30.086154,1.062313],[30.468508,1.583805],[30.85267,1.849396],[31.174149,2.204465],[30.77332,2.33989],[30.83385,3.50917],[31.24556,3.7819],[31.88145,3.55827],[32.68642,3.79232],[33.39,3.79],[34.005,4.249885],[34.47913,3.5556],[34.59 [...]
+{"type":"Feature","properties":{"name":"Ukraine"},"geometry":{"type":"Polygon","coordinates":[[[31.785998,52.101678],[32.159412,52.061267],[32.412058,52.288695],[32.715761,52.238465],[33.7527,52.335075],[34.391731,51.768882],[34.141978,51.566413],[34.224816,51.255993],[35.022183,51.207572],[35.377924,50.773955],[35.356116,50.577197],[36.626168,50.225591],[37.39346,50.383953],[38.010631,49.915662],[38.594988,49.926462],[40.069058,49.601055],[40.080789,49.30743],[39.674664,48.783818],[39.8 [...]
+{"type":"Feature","properties":{"name":"Uruguay"},"geometry":{"type":"Polygon","coordinates":[[[-57.625133,-30.216295],[-56.976026,-30.109686],[-55.973245,-30.883076],[-55.60151,-30.853879],[-54.572452,-31.494511],[-53.787952,-32.047243],[-53.209589,-32.727666],[-53.650544,-33.202004],[-53.373662,-33.768378],[-53.806426,-34.396815],[-54.935866,-34.952647],[-55.67409,-34.752659],[-56.215297,-34.859836],[-57.139685,-34.430456],[-57.817861,-34.462547],[-58.427074,-33.909454],[-58.349611,-33 [...]
+{"type":"Feature","properties":{"name":"United States of America"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-155.54211,19.08348],[-155.68817,18.91619],[-155.93665,19.05939],[-155.90806,19.33888],[-156.07347,19.70294],[-156.02368,19.81422],[-155.85008,19.97729],[-155.91907,20.17395],[-155.86108,20.26721],[-155.78505,20.2487],[-155.40214,20.07975],[-155.22452,19.99302],[-155.06226,19.8591],[-154.80741,19.50871],[-154.83147,19.45328],[-155.22217,19.23972],[-155.54211,19.08348]]], [...]
+{"type":"Feature","properties":{"name":"Uzbekistan"},"geometry":{"type":"Polygon","coordinates":[[[66.518607,37.362784],[66.54615,37.974685],[65.215999,38.402695],[64.170223,38.892407],[63.518015,39.363257],[62.37426,40.053886],[61.882714,41.084857],[61.547179,41.26637],[60.465953,41.220327],[60.083341,41.425146],[59.976422,42.223082],[58.629011,42.751551],[57.78653,42.170553],[56.932215,41.826026],[57.096391,41.32231],[55.968191,41.308642],[55.928917,44.995858],[58.503127,45.586804],[58 [...]
+{"type":"Feature","properties":{"name":"Venezuela"},"geometry":{"type":"Polygon","coordinates":[[[-71.331584,11.776284],[-71.360006,11.539994],[-71.94705,11.423282],[-71.620868,10.96946],[-71.633064,10.446494],[-72.074174,9.865651],[-71.695644,9.072263],[-71.264559,9.137195],[-71.039999,9.859993],[-71.350084,10.211935],[-71.400623,10.968969],[-70.155299,11.375482],[-70.293843,11.846822],[-69.943245,12.162307],[-69.5843,11.459611],[-68.882999,11.443385],[-68.233271,10.885744],[-68.194127, [...]
+{"type":"Feature","properties":{"name":"Vietnam"},"geometry":{"type":"Polygon","coordinates":[[[108.05018,21.55238],[106.715068,20.696851],[105.881682,19.75205],[105.662006,19.058165],[106.426817,18.004121],[107.361954,16.697457],[108.269495,16.079742],[108.877107,15.276691],[109.33527,13.426028],[109.200136,11.666859],[108.36613,11.008321],[107.220929,10.364484],[106.405113,9.53084],[105.158264,8.59976],[104.795185,9.241038],[105.076202,9.918491],[104.334335,10.486544],[105.199915,10.88 [...]
+{"type":"Feature","properties":{"name":"Vanuatu"},"geometry":{"type":"MultiPolygon","coordinates":[[[[167.844877,-16.466333],[167.515181,-16.59785],[167.180008,-16.159995],[167.216801,-15.891846],[167.844877,-16.466333]]],[[[167.107712,-14.93392],[167.270028,-15.740021],[167.001207,-15.614602],[166.793158,-15.668811],[166.649859,-15.392704],[166.629137,-14.626497],[167.107712,-14.93392]]]]},"id":"VUT"},
+{"type":"Feature","properties":{"name":"West Bank"},"geometry":{"type":"Polygon","coordinates":[[[35.545665,32.393992],[35.545252,31.782505],[35.397561,31.489086],[34.927408,31.353435],[34.970507,31.616778],[35.225892,31.754341],[34.974641,31.866582],[35.18393,32.532511],[35.545665,32.393992]]]},"id":"PSE"},
+{"type":"Feature","properties":{"name":"Yemen"},"geometry":{"type":"Polygon","coordinates":[[[53.108573,16.651051],[52.385206,16.382411],[52.191729,15.938433],[52.168165,15.59742],[51.172515,15.17525],[49.574576,14.708767],[48.679231,14.003202],[48.238947,13.94809],[47.938914,14.007233],[47.354454,13.59222],[46.717076,13.399699],[45.877593,13.347764],[45.62505,13.290946],[45.406459,13.026905],[45.144356,12.953938],[44.989533,12.699587],[44.494576,12.721653],[44.175113,12.58595],[43.48295 [...]
+{"type":"Feature","properties":{"name":"South Africa"},"geometry":{"type":"Polygon","coordinates":[[[31.521001,-29.257387],[31.325561,-29.401978],[30.901763,-29.909957],[30.622813,-30.423776],[30.055716,-31.140269],[28.925553,-32.172041],[28.219756,-32.771953],[27.464608,-33.226964],[26.419452,-33.61495],[25.909664,-33.66704],[25.780628,-33.944646],[25.172862,-33.796851],[24.677853,-33.987176],[23.594043,-33.794474],[22.988189,-33.916431],[22.574157,-33.864083],[21.542799,-34.258839],[20 [...]
+{"type":"Feature","properties":{"name":"Zambia"},"geometry":{"type":"Polygon","coordinates":[[[32.759375,-9.230599],[33.231388,-9.676722],[33.485688,-10.525559],[33.31531,-10.79655],[33.114289,-11.607198],[33.306422,-12.435778],[32.991764,-12.783871],[32.688165,-13.712858],[33.214025,-13.97186],[30.179481,-14.796099],[30.274256,-15.507787],[29.516834,-15.644678],[28.947463,-16.043051],[28.825869,-16.389749],[28.467906,-16.4684],[27.598243,-17.290831],[27.044427,-17.938026],[26.706773,-17 [...]
+{"type":"Feature","properties":{"name":"Zimbabwe"},"geometry":{"type":"Polygon","coordinates":[[[31.191409,-22.25151],[30.659865,-22.151567],[30.322883,-22.271612],[29.839037,-22.102216],[29.432188,-22.091313],[28.794656,-21.639454],[28.02137,-21.485975],[27.727228,-20.851802],[27.724747,-20.499059],[27.296505,-20.39152],[26.164791,-19.293086],[25.850391,-18.714413],[25.649163,-18.536026],[25.264226,-17.73654],[26.381935,-17.846042],[26.706773,-17.961229],[27.044427,-17.938026],[27.59824 [...]
+]}
\ No newline at end of file
diff --git a/test/exporter.js b/test/exporter.js
new file mode 100644
index 0000000..320aade
--- /dev/null
+++ b/test/exporter.js
@@ -0,0 +1,17 @@
+var Config = require('../src/Config.js').Config,
+    Project = require('../src/back/Project.js').Project,
+    fs = require('fs'),
+    assert = require('assert');
+
+describe('#XML()', function () {
+
+    it('should export in XML', function () {
+        var config = new Config(__dirname, 'config.yml'),
+            project = new Project(config, 'test/data/minimalist-project.mml');
+        project.load();
+        project.export({format: 'xml'}, function (err, data) {
+            assert.equal(data + '\n', fs.readFileSync('test/data/minimalist-project.xml', 'utf8'));
+        });
+    });
+
+});
diff --git a/test/geoutils.js b/test/geoutils.js
new file mode 100644
index 0000000..4b33c28
--- /dev/null
+++ b/test/geoutils.js
@@ -0,0 +1,10 @@
+var GeoUtils = require('../src/back/GeoUtils.js'),
+    assert = require('assert');
+
+describe('#zoomLatLngToXY()', function () {
+
+    it('0/-85/-179.9999 lat should return 0/0', function () {
+        assert.deepEqual(GeoUtils.zoomLatLngToXY(0, -85, -179.99978348919964), [0, 0]);
+    });
+
+});
diff --git a/test/loader.js b/test/loader.js
new file mode 100644
index 0000000..2113f47
--- /dev/null
+++ b/test/loader.js
@@ -0,0 +1,27 @@
+var Config = require('../src/Config.js').Config,
+    Project = require('../src/back/Project.js').Project,
+    assert = require('assert');
+
+describe('#MML()', function () {
+
+    it('can load an MML file', function () {
+        var config = new Config(__dirname, 'config.yml'),
+            project = new Project(config, 'test/data/minimalist-project.mml');
+        project.load();
+        assert(project.mml);
+        assert.equal(project.mml.name, 'ProjectName');
+    });
+
+});
+
+describe('#YAML()', function () {
+
+    it('can load a YAML file', function () {
+        var config = new Config(__dirname, 'test/data/config.yml'),
+            project = new Project(config, 'test/data/minimalist-project.yml');
+        project.load();
+        assert(project.mml);
+        assert.equal(project.mml.name, 'ProjectName');
+    });
+
+});
diff --git a/test/tile.js b/test/tile.js
new file mode 100644
index 0000000..8eabfbb
--- /dev/null
+++ b/test/tile.js
@@ -0,0 +1,83 @@
+var Config = require('../src/Config.js').Config,
+    Project = require('../src/back/Project.js').Project,
+    Tile = require('../src/back/Tile.js').Tile,
+    fs = require('fs'),
+    assert = require('assert'),
+    mapnik = require('mapnik');
+
+var trunc_6 = function(key, val) {
+    return val.toFixed ? Number(val.toFixed(6)) : val;
+}
+
+function compareGeoJSON(json1, json2) {
+    if (typeof json1 === 'string') json1 = JSON.parse(json1);
+    if (typeof json2 === 'string') json2 = JSON.parse(json2);
+
+    json1 = JSON.parse(JSON.stringify(json1, trunc_6));
+    json2 = JSON.parse(JSON.stringify(json2, trunc_6));
+
+    return assert.deepEqual(json1, json2);
+}
+
+describe('#Tile()', function () {
+    var config, project, map;
+
+    before(function () {
+        config = new Config(__dirname);
+        project = new Project(config, 'test/data/world/project.yml');
+        map = new mapnik.Map(256, 256);
+        project.render();
+        map.fromStringSync(project.xml, {base: project.root});
+    });
+
+    describe('#render()', function () {
+
+        it('should render a PNG of the world', function (done) {
+            var tile = new Tile(0, 0, 0);
+            tile.render(project, map, function (err, im) {
+                if (err) throw err;
+                im.encode('png', function (err, buffer) {
+                    if (err) throw err;
+                    assert.deepEqual(buffer, fs.readFileSync('test/data/expected/tile.world.0.0.0.png'));
+                    done();
+                });
+            });
+        });
+
+        it('should render a PNG of Hispaniola', function (done) {
+            var tile = new Tile(6, 19, 28);
+            tile.render(project, map, function (err, im) {
+                if (err) throw err;
+                im.encode('png', function (err, buffer) {
+                    if (err) throw err;
+                    assert.deepEqual(buffer, fs.readFileSync('test/data/expected/tile.world.6.19.28.png'));
+                    done();
+                });
+            });
+        });
+
+    });
+
+    describe('#renderToVector()', function () {
+
+        it('should render a GeoJSON', function (done) {
+            var tile = new Tile(6, 19, 28);
+            tile.renderToVector(project, map, function (err, vtile) {
+                if (err) throw err;
+                compareGeoJSON(vtile.toGeoJSON('__all__'), JSON.parse(fs.readFileSync('test/data/expected/tile.world.6.19.28.geojson')));
+                done();
+            });
+        });
+
+        it('should render a PBF', function (done) {
+            var tile = new Tile(6, 19, 28);
+            tile.renderToVector(project, map, function (err, vtile) {
+                if (err) throw err;
+                assert.deepEqual(vtile.getData(), fs.readFileSync('test/data/expected/tile.world.6.19.28.pbf'));
+                done();
+            });
+        });
+
+    });
+
+});
diff --git a/test/utils.js b/test/utils.js
new file mode 100644
index 0000000..d2c646b
--- /dev/null
+++ b/test/utils.js
@@ -0,0 +1,18 @@
+var Utils = require('../src/back/Utils.js'),
+    assert = require('assert');
+
+describe('#tree()', function () {
+
+    it('should retrieve dir tree', function () {
+        var files = Utils.tree('./test/data/tree');
+        assert.deepEqual(
+            files.map(function (x) {return x.path;}),
+            [
+                'test/data/tree/afile.txt',
+                'test/data/tree/subdir',
+                'test/data/tree/subdir/anotherfile.js',
+                'test/data/tree/subdir/anothersubdir',
+                'test/data/tree/subdir/anothersubdir/yetafile.csv' ]);
+    });
+
+});

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



More information about the Pkg-grass-devel mailing list