[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
+
+[](https://gitter.im/kosmtik/kosmtik?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[](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
+
+
+
+
+## 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