commit 431fb56ad2d48fa5f4385676a110427d3e3061e2 Author: Simon Tokumine Date: Mon Sep 5 00:00:41 2011 +0100 windshaft for cartodb diff --git a/README.md b/README.md new file mode 100644 index 00000000..cf4c3c29 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +Windshaft-CartoDB +================== + diff --git a/app.js b/app.js new file mode 100755 index 00000000..a1c958c9 --- /dev/null +++ b/app.js @@ -0,0 +1,36 @@ +/* +* Windshaft +* =============== +* +* ./app.js [environment] +* +* environments: [development, production] +*/ + + +// sanity check +var ENV = process.argv[2] +if (ENV != 'development' && ENV != 'production'){ + console.error("\nnode app.js [environment]"); + console.error("environments: [development, production]\n"); + process.exit(1); +} + +var _ = require('underscore'); + +// set environment specific variables +global.settings = require(__dirname + '/config/settings'); +global.environment = require(__dirname + '/config/environments/' + ENV); +_.extend(global.settings, global.environment); + +var Windshaft = require('windshaft'); +var serverOptions = require('./lib/cartodb/server_options'); + +// boot +var ws = new Windshaft.Server(serverOptions); +ws.listen(global.environment.windshaft_port); +console.log("Windshaft tileserver started on port " + global.environment.windshaft_port); + + + + diff --git a/config/environments/development.js b/config/environments/development.js new file mode 100644 index 00000000..299b839b --- /dev/null +++ b/config/environments/development.js @@ -0,0 +1,7 @@ +module.exports.name = 'development'; +module.exports.postgres = {user: 'postgres', host: '127.0.0.1', port: 5432}; +module.exports.redis = {host: '127.0.0.1', + port: 6379, + idleTimeoutMillis: 1, + reapIntervalMillis: 1}; +module.exports.windshaft_port = 8080; \ No newline at end of file diff --git a/config/environments/production.js b/config/environments/production.js new file mode 100644 index 00000000..348db13d --- /dev/null +++ b/config/environments/production.js @@ -0,0 +1,4 @@ +module.exports.name = 'production'; +module.exports.postgres = {user: 'publicuser', host: '10.211.14.63', port: 6543}; +module.exports.redis = {host: '10.211.14.63', port: 6379}; +module.exports.windshaft_port = 8080; \ No newline at end of file diff --git a/config/environments/test.js b/config/environments/test.js new file mode 100644 index 00000000..7a710253 --- /dev/null +++ b/config/environments/test.js @@ -0,0 +1,7 @@ +module.exports.name = 'test'; +module.exports.postgres = {user: 'postgres', host: '127.0.0.1', port: 5432}; +module.exports.redis = {host: '127.0.0.1', + port: 6379, + idleTimeoutMillis: 1, + reapIntervalMillis: 1}; +module.exports.windshaft_port = 8080; \ No newline at end of file diff --git a/config/settings.js b/config/settings.js new file mode 100644 index 00000000..76639019 --- /dev/null +++ b/config/settings.js @@ -0,0 +1 @@ +module.exports.oneDay = 86400000; \ No newline at end of file diff --git a/lib/cartodb/carto_data.js b/lib/cartodb/carto_data.js new file mode 100644 index 00000000..f4a3a34f --- /dev/null +++ b/lib/cartodb/carto_data.js @@ -0,0 +1,134 @@ +/** + * User: simon + * Date: 30/08/2011 + * Time: 21:10 + * Desc: CartoDB helper. + * Retrieves dbname (based on subdomain/username) + * and geometry type from the redis stores of cartodb + */ + +var RedisPool = require("./redis_pool") + , _ = require('underscore') + , Step = require('step'); + +module.exports = function() { + var redis_pool = new RedisPool(); + + + var me = { + user_metadata_db: 5, + table_metadata_db: 0, + user_key: "rails:users:<%= username %>", + table_key: "rails:<%= database_name %>:<%= table_name %>" + }; + + + + /** + * Get the database name for this particular subdomain/username + * + * @param req - standard express req object. importantly contains host information + * @param callback + */ + me.getDatabase = function(req, callback) { + // strip subdomain from header host + var username = req.headers.host.split('.')[0] + var redisKey = _.template(this.user_key, {username: username}); + + this.retrieve(this.user_metadata_db, redisKey, 'database_name', callback); + }; + + + + /** + * Get the user id for this particular subdomain/username + * + * @param req - standard express req object. importantly contains host information + * @param callback + */ + me.getId= function(req, callback) { + // strip subdomain from header host + var username = req.headers.host.split('.')[0] + var redisKey = _.template(this.user_key, {username: username}); + + this.retrieve(this.user_metadata_db, redisKey, 'id', callback); + }; + + + + /** + * Get the geometry type for this particular table; + * @param req - standard req object. Importantly contains table and host information + * @param callback + */ + me.getGeometryType = function(req, callback){ + var that = this; + + Step( + function(){ + that.getDatabase(req, this) + }, + function(err, data){ + if (err) throw err; + var redisKey = _.template(that.table_key, {database_name: data, table_name: req.params.table}); + + that.retrieve(that.table_metadata_db, redisKey, 'the_geom_type', this); + }, + function(err, data){ + if (err) throw err; + callback(err, data); + } + ); + }; + + + me.getInfowindow = function(req, callback){ + var that = this; + + Step( + function(){ + that.getDatabase(req, this); + }, + function(err, data){ + if (err) throw err; + var redisKey = _.template(that.table_key, {database_name: data, table_name: req.params.table}); + + that.retrieve(that.table_metadata_db, redisKey, 'infowindow', this); + }, + function(err, data){ + if (err) throw err; + callback(err, data); + } + ); + }; + + + + /** + * Make a HASH data access call to Redis + * + * @param redisKey - the base redis key where the metadata hash lives + * @param hashKey - the specific metadata you want to retrieve + * @param callback - function to pass metadata too. err,data args + */ + me.retrieve = function(db, redisKey, hashKey, callback) { + var redisClient; + + Step( + function getRedisClient() { + redis_pool.acquire(db, this); + }, + function lookupMetadata(err, data) { + redisClient = data; + redisClient.HGET(redisKey, hashKey, this); + }, + function releaseRedisClient(err, data) { + if (err) throw err; + redis_pool.release(db, redisClient); + callback(err, data); + } + ); + }; + + return me; +}(); \ No newline at end of file diff --git a/lib/cartodb/redis_pool.js b/lib/cartodb/redis_pool.js new file mode 100644 index 00000000..ea5c1bd5 --- /dev/null +++ b/lib/cartodb/redis_pool.js @@ -0,0 +1,77 @@ +/** + * RedisPool. A database specific redis pooling lib + * + */ + +var redis = require('redis') + , _ = require('underscore') + , Pool = require('generic-pool').Pool; + +// constructor. +// +// - `opts` {Object} optional config for redis and pooling +var RedisPool = function(opts){ + var opts = opts || {}; + var defaults = { + host: '127.0.0.1', + port: '6379', + max: 50, + idleTimeoutMillis: 10000, + reapIntervalMillis: 1000, + log: false + }; + var options = _.defaults(opts, defaults) + + var me = { + pools: {} // cached pools by DB name + }; + + // Acquire resource. + // + // - `database` {String} redis database name + // - `callback` {Function} callback to call once acquired. Takes the form + // `callback(err, resource)` + me.acquire = function(database, callback) { + if (!this.pools[database]) { + this.pools[database] = this.makePool(database); + } + this.pools[database].acquire(function(err,resource) { + callback(err, resource); + }); + }; + + // Release resource. + // + // - `database` {String} redis database name + // - `resource` {Object} resource object to release + me.release = function(database, resource) { + this.pools[database] && this.pools[database].release(resource); + }; + + // Factory for pool objects. + me.makePool = function(database) { + return Pool({ + name: database, + create: function(callback){ + var client = redis.createClient(options.port, options.host); + client.on('connect', function () { + client.send_anyway = true; + client.select(database); + client.send_anyway = false; + }); + return callback(null, client); + }, + destroy: function(client) { + return client.quit(); + }, + max: options.max, + idleTimeoutMillis: options.idleTimeoutMillis, + reapIntervalMillis: options.reapIntervalMillis, + log: options.log + }); + }; + + return me; +}; + +module.exports = RedisPool; \ No newline at end of file diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js new file mode 100644 index 00000000..d0589140 --- /dev/null +++ b/lib/cartodb/server_options.js @@ -0,0 +1,73 @@ +var _ = require('underscore') + , Step = require('step') + , cartoData = require('./carto_data'); + +module.exports = function(){ + var me = { + base_url: '/tiles/:table', + grainstore: {datasource: global.environment.postgres}, + redis: global.environment.redis +}; + +/** + * Whitelist input and get database name & default geometry type from + * subdomain/user metadata held in CartoDB Redis + * @param req - standard express request obj. Should have host & table + * @param callback + */ +me.req2params = function(req, callback){ + + // Whitelist query parameters and attach format + var good_query = ['sql', 'geom_type', 'cache_buster','callback', 'interactivity']; + var bad_query = _.difference(_.keys(req.query), good_query); + + _.each(bad_query, function(key){ delete req.query[key]; }); + req.params = _.extend({}, req.params); // shuffle things as request is a strange array/object + + // bring all query values onto req.params object + _.extend(req.params, req.query); + + // for cartodb, ensure interactivity is cartodb_id + req.params.interactivity = 'cartodb_id'; + + Step( + function getDatabase(){ + cartoData.getDatabase(req, this); + }, + function getGeometryType(err, data){ + if (err) throw err; + _.extend(req.params, {dbname:data}); + + cartoData.getGeometryType(req, this); + }, + function finishSetup(err, data){ + if (err) throw err; + if (!_.isNull(data)) + _.extend(req.params, {geom_type: data}); + + callback(err, req); + } + ); +}; + +/** + * Little helper method to get the current list of infowindow variables and return to client + * @param req + * @param callback + */ +me.getInfowindow = function(req, callback){ + var that = this; + + Step( + function(){ + that.req2params(req, this); + }, + function(err, data){ + if (err) throw err; + cartoData.getInfowindow(data, callback); + } + ); +}; + +return me; +}(); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..425806ce --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "private": true, + "name": "windshaft-cartodb", + "version": "0.0.1", + "description": "A map tile server for CartoDB", + "url": "https://github.com/Vizzuality/Windshaft-cartodb", + "licenses": [{ "type": "Copyright Vizzuality" }], + "repositories": [{ + "type": "git", + "url": "git://github.com/Vizzuality/Windshaft-cartodb.git" + }], + "author": { + "name": "Simon Tokumine, Vizzuality", + "url": "http://vizzuality.com", + "email": "simon@vizzuality.com" + }, + "dependencies": { + "underscore" : "1.1.x", + "windshaft" : "0.0.4", + "step": "0.0.x", + "generic-pool": "1.0.x", + "redis": "0.6.7", + "hiredis": "0.1.12" + }, + "devDependencies": { + "expresso": "0.8.x" + }, + "scripts": { + "test": "which expresso | sh" + } +} \ No newline at end of file diff --git a/test/acceptance/server.js b/test/acceptance/server.js new file mode 100644 index 00000000..bda87bc4 --- /dev/null +++ b/test/acceptance/server.js @@ -0,0 +1,214 @@ +var assert = require('assert'); +var tests = module.exports = {}; +var _ = require('underscore'); +var querystring = require('querystring'); +require(__dirname + '/../test_helper'); + +var Windshaft = require(__dirname + '/../../lib/windshaft'); +var serverOptions = require(__dirname + '/../../lib/cartodb/server_options'); +var server = new Windshaft.Server(serverOptions); + +tests['true'] = function() { + assert.ok(true); +}; + +tests["get call to server returns 200"] = function(){ + assert.response(server, { + url: '/', + method: 'GET' + },{ + status: 200 + }); +}; + +tests["get'ing blank style returns default style"] = function(){ + assert.response(server, { + headers: {host: 'vizzuality.localhost.lan'}, + url: '/tiles/my_table/style', + method: 'GET' + },{ + status: 200, + body: '{"style":"#my_table {marker-fill: #FF6600;marker-opacity: 1;marker-width: 8;marker-line-color: white;marker-line-width: 3;marker-line-opacity: 0.9;marker-placement: point;marker-type: ellipse;marker-allow-overlap: true;}"}' + }); +}; + + + +tests["post'ing no style returns 400 with errors"] = function(){ + assert.response(server, { + headers: {host: 'vizzuality.localhost.lan'}, + url: '/tiles/my_table/style', + method: 'POST' + },{ + status: 400, + body: '{"error":"must sent style information"}' + }); +}; + + + +tests["post'ing bad style returns 400 with error"] = function(){ + assert.response(server, { + url: '/tiles/my_table3/style', + method: 'POST', + headers: {host: 'vizzuality.localhost.lan', 'Content-Type': 'application/x-www-form-urlencoded' }, + data: querystring.stringify({style: '#my_table3{backgxxxxxround-color:#fff;}'}) + },{ + status: 400, + body: JSON.stringify(['style.mss:1:11 Unrecognized rule: backgxxxxxround-color']) + }); +}; + + + +tests["post'ing multiple bad styles returns 400 with error array"] = function(){ + assert.response(server, { + url: '/tiles/my_table4/style', + method: 'POST', + headers: {host: 'vizzuality.localhost.lan', 'Content-Type': 'application/x-www-form-urlencoded' }, + data: querystring.stringify({style: '#my_table4{backgxxxxxround-color:#fff;foo:bar}'}) + },{ + status: 400, + body: JSON.stringify([ 'style.mss:1:11 Unrecognized rule: backgxxxxxround-color', 'style.mss:1:38 Unrecognized rule: foo' ]) + }); +}; + + + +tests["post'ing good style returns 200"] = function(){ + assert.response(server, { + url: '/tiles/my_table5/style', + method: 'POST', + headers: {host: 'vizzuality.localhost.lan', 'Content-Type': 'application/x-www-form-urlencoded' }, + data: querystring.stringify({style: '#my_table5{background-color:#fff;}'}) + },{ + status: 200 + }); +}; + + + + +tests["post'ing good style returns 200 then getting returns original style"] = function(){ + var style = '#my_table5{background-color:#fff;}'; + assert.response(server, { + url: '/tiles/my_table5/style', + method: 'POST', + headers: {host: 'vizzuality.localhost.lan', 'Content-Type': 'application/x-www-form-urlencoded' }, + data: querystring.stringify({style: style}) + },{ + status: 200 + }); + + + assert.response(server, { + headers: {host: 'vizzuality.localhost.lan'}, + url: '/tiles/my_table5/style', + method: 'GET' + },{ + status: 200, + body: JSON.stringify({style: style}) + }); +}; + + +tests["get'ing blank infowindow returns blank"] = function(){ + assert.response(server, { + headers: {host: 'vizzuality.localhost.lan'}, + url: '/tiles/my_tablez/infowindow', + method: 'GET' + },{ + status: 200, + body: '{"infowindow":null}' + }); +}; + +tests["get'ing blank infowindow with callback returns blank with callback"] = function(){ + assert.response(server, { + headers: {host: 'vizzuality.localhost.lan'}, + url: '/tiles/my_tablez/infowindow?callback=simon', + method: 'GET' + },{ + status: 200, + body: 'simon({"infowindow":null});' + }); +}; + + +// note requires: +// SELECT 0 +// HSET rails:cartodb_user_123_db:my_table infowindow "this, that, the other" +tests["get'ing completed infowindow with callback returns information with callback"] = function(){ + assert.response(server, { + headers: {host: 'vizzuality.localhost.lan'}, + url: '/tiles/my_table/infowindow?callback=simon', + method: 'GET' + },{ + status: 200, + body: 'simon({"infowindow":"this, that, the other"});' + }); +}; + + + +tests["get'ing a tile with default style should return an image"] = function(){ + assert.response(server, { + headers: {host: 'vizzuality.localhost.lan'}, + url: '/tiles/gadm4/6/31/24.png?geom_type=polygon', + method: 'GET' + },{ + status: 200, + headers: { 'Content-Type': 'image/png' } + }); +}; + +tests["get'ing a json with default style should return an grid"] = function(){ + assert.response(server, { + headers: {host: 'vizzuality.localhost.lan'}, + url: '/tiles/gadm4/6/31/24.grid.json', + method: 'GET' + },{ + status: 200, + headers: { 'Content-Type': 'text/javascript; charset=utf-8; charset=utf-8' } + }); +}; + + +tests["get'ing a json with default style and sql should return a constrained grid"] = function(){ + var sql = querystring.stringify({sql: "(SELECT * FROM gadm4 WHERE name_2 = 'Murcia') as foo"}) + assert.response(server, { + headers: {host: 'vizzuality.localhost.lan'}, + url: '/tiles/gadm4/6/31/24.grid.json?' + sql, + method: 'GET' + },{ + status: 200, + headers: { 'Content-Type': 'text/javascript; charset=utf-8; charset=utf-8' } + }); +}; + + + +tests["get'ing a tile with default style and sql should return a constrained image"] = function(){ + var sql = querystring.stringify({sql: "(SELECT * FROM gadm4 WHERE name_2 = 'Murcia') as foo"}); + assert.response(server, { + headers: {host: 'vizzuality.localhost.lan'}, + url: '/tiles/gadm4/6/31/24.png?' + sql, + method: 'GET' + },{ + status: 200, + headers: { 'Content-Type': 'image/png' } + }); +}; + + +tests["get'ing a tile with default style and complex sql should return a constrained image"] = function(){ + var sql = querystring.stringify({sql: "(SELECT * FROM gadm4 WHERE name_2 = 'Murcia' AND id_1 > 950) as foo"}) + assert.response(server, { + headers: {host: 'vizzuality.localhost.lan'}, + url: '/tiles/gadm4/6/31/24.png?' + sql, + method: 'GET' + },{ + status: 200, + headers: { 'Content-Type': 'image/png' } + }); +}; \ No newline at end of file diff --git a/test/monkey/images/layers.png b/test/monkey/images/layers.png new file mode 100755 index 00000000..9be965fc Binary files /dev/null and b/test/monkey/images/layers.png differ diff --git a/test/monkey/images/marker-shadow.png b/test/monkey/images/marker-shadow.png new file mode 100755 index 00000000..a64f6a67 Binary files /dev/null and b/test/monkey/images/marker-shadow.png differ diff --git a/test/monkey/images/marker.png b/test/monkey/images/marker.png new file mode 100755 index 00000000..bef032e6 Binary files /dev/null and b/test/monkey/images/marker.png differ diff --git a/test/monkey/images/popup-close.png b/test/monkey/images/popup-close.png new file mode 100755 index 00000000..c8faec5e Binary files /dev/null and b/test/monkey/images/popup-close.png differ diff --git a/test/monkey/images/zoom-in.png b/test/monkey/images/zoom-in.png new file mode 100755 index 00000000..9f473d64 Binary files /dev/null and b/test/monkey/images/zoom-in.png differ diff --git a/test/monkey/images/zoom-out.png b/test/monkey/images/zoom-out.png new file mode 100755 index 00000000..f0a5b5d6 Binary files /dev/null and b/test/monkey/images/zoom-out.png differ diff --git a/test/monkey/index.html b/test/monkey/index.html new file mode 100644 index 00000000..47173833 --- /dev/null +++ b/test/monkey/index.html @@ -0,0 +1,23 @@ + + + + Windshaft test + + + + + + +
+ + + + + \ No newline at end of file diff --git a/test/monkey/leaflet.css b/test/monkey/leaflet.css new file mode 100755 index 00000000..4cb788c7 --- /dev/null +++ b/test/monkey/leaflet.css @@ -0,0 +1,321 @@ +/* required styles */ + +.leaflet-map-pane, +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-tile-pane, +.leaflet-overlay-pane, +.leaflet-shadow-pane, +.leaflet-marker-pane, +.leaflet-popup-pane, +.leaflet-overlay-pane svg, +.leaflet-zoom-box, +.leaflet-image-layer { /* TODO optimize classes */ + position: absolute; + } +.leaflet-container { + overflow: hidden; + } +.leaflet-tile-pane { + -webkit-transform: translate3d(0,0,0); + } +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow { + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; + } +.leaflet-marker-icon, +.leaflet-marker-shadow { + display: block; + } +.leaflet-clickable { + cursor: pointer; + } +.leaflet-container img { + max-width: auto; + } + +.leaflet-tile-pane { z-index: 2; } +.leaflet-overlay-pane { z-index: 3; } +.leaflet-shadow-pane { z-index: 4; } +.leaflet-marker-pane { z-index: 5; } +.leaflet-popup-pane { z-index: 6; } + +.leaflet-zoom-box { + width: 0; + height: 0; + } + +.leaflet-tile { + visibility: hidden; + } +.leaflet-tile-loaded { + visibility: inherit; + } + +a.leaflet-active { + outline: 2px solid orange; + } + + +/* Leaflet controls */ + +.leaflet-control { + position: relative; + z-index: 7; + } +.leaflet-top, +.leaflet-bottom { + position: absolute; + } +.leaflet-top { + top: 0; + } +.leaflet-right { + right: 0; + } +.leaflet-bottom { + bottom: 0; + } +.leaflet-left { + left: 0; + } +.leaflet-control { + float: left; + clear: both; + } +.leaflet-right .leaflet-control { + float: right; + } +.leaflet-top .leaflet-control { + margin-top: 10px; + } +.leaflet-bottom .leaflet-control { + margin-bottom: 10px; + } +.leaflet-left .leaflet-control { + margin-left: 10px; + } +.leaflet-right .leaflet-control { + margin-right: 10px; + } + +.leaflet-control-zoom, .leaflet-control-layers { + -moz-border-radius: 7px; + -webkit-border-radius: 7px; + border-radius: 7px; + } +.leaflet-control-zoom { + padding: 5px; + background: rgba(0, 0, 0, 0.25); + } +.leaflet-control-zoom a { + background-color: rgba(255, 255, 255, 0.75); + } +.leaflet-control-zoom a, .leaflet-control-layers a { + background-position: 50% 50%; + background-repeat: no-repeat; + display: block; + } +.leaflet-control-zoom a { + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; + width: 19px; + height: 19px; + } +.leaflet-control-zoom a:hover { + background-color: #fff; + } +.leaflet-big-buttons .leaflet-control-zoom a { + width: 27px; + height: 27px; + } +.leaflet-control-zoom-in { + background-image: url(images/zoom-in.png); + margin-bottom: 5px; + } +.leaflet-control-zoom-out { + background-image: url(images/zoom-out.png); + } + +.leaflet-control-layers { + -moz-box-shadow: 0 0 7px #999; + -webkit-box-shadow: 0 0 7px #999; + box-shadow: 0 0 7px #999; + + background: #f8f8f9; + } +.leaflet-control-layers a { + background-image: url(images/layers.png); + width: 36px; + height: 36px; + } +.leaflet-big-buttons .leaflet-control-layers a { + width: 44px; + height: 44px; + } +.leaflet-control-layers .leaflet-control-layers-list, +.leaflet-control-layers-expanded .leaflet-control-layers-toggle { + display: none; + } +.leaflet-control-layers-expanded .leaflet-control-layers-list { + display: block; + position: relative; + } +.leaflet-control-layers-expanded { + padding: 6px 10px 6px 6px; + font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; + color: #333; + background: #fff; + } +.leaflet-control-layers input { + margin-top: 2px; + position: relative; + top: 1px; + } +.leaflet-control-layers label { + display: block; + } +.leaflet-control-layers-separator { + height: 0; + border-top: 1px solid #ddd; + margin: 5px -10px 5px -6px; + } + +.leaflet-container .leaflet-control-attribution { + margin: 0; + padding: 0 5px; + + font: 11px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; + color: #333; + + background-color: rgba(255, 255, 255, 0.7); + + -moz-box-shadow: 0 0 7px #ccc; + -webkit-box-shadow: 0 0 7px #ccc; + box-shadow: 0 0 7px #ccc; + } + + +/* Fade animations */ + +.leaflet-fade-anim .leaflet-tile { + opacity: 0; + + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + -o-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; + } +.leaflet-fade-anim .leaflet-tile-loaded { + opacity: 1; + } + +.leaflet-fade-anim .leaflet-popup { + opacity: 0; + + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + -o-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; + } +.leaflet-fade-anim .leaflet-map-pane .leaflet-popup { + opacity: 1; + } + +.leaflet-zoom-anim .leaflet-tile { + -webkit-transition: none; + -moz-transition: none; + -o-transition: none; + transition: none; + } + +.leaflet-zoom-anim .leaflet-objects-pane { + visibility: hidden; + } + + +/* Popup layout */ + +.leaflet-popup { + position: absolute; + text-align: center; + -webkit-transform: translate3d(0,0,0); + } +.leaflet-popup-content-wrapper { + padding: 1px; + text-align: left; + } +.leaflet-popup-content { + margin: 19px; + } +.leaflet-popup-tip-container { + margin: 0 auto; + width: 40px; + height: 16px; + position: relative; + overflow: hidden; + } +.leaflet-popup-tip { + width: 15px; + height: 15px; + padding: 1px; + + margin: -8px auto 0; + + -moz-transform: rotate(45deg); + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + -o-transform: rotate(45deg); + transform: rotate(45deg); + } +.leaflet-popup-close-button { + position: absolute; + top: 9px; + right: 9px; + + width: 10px; + height: 10px; + + overflow: hidden; + } +.leaflet-popup-content p { + margin: 18px 0; + } + + +/* Visual appearance */ + +.leaflet-container { + background: #ddd; + } +.leaflet-container a { + color: #0078A8; + } +.leaflet-zoom-box { + border: 2px dotted #05f; + background: white; + opacity: 0.5; + } +.leaflet-popup-content-wrapper, .leaflet-popup-tip { + background: white; + + box-shadow: 0 1px 10px #888; + -moz-box-shadow: 0 1px 10px #888; + -webkit-box-shadow: 0 1px 14px #999; + } +.leaflet-popup-content-wrapper { + -moz-border-radius: 20px; + -webkit-border-radius: 20px; + border-radius: 20px; + } +.leaflet-popup-content { + font: 12px/1.4 "Helvetica Neue", Arial, Helvetica, sans-serif; + } +.leaflet-popup-close-button { + background: white url(images/popup-close.png); + } \ No newline at end of file diff --git a/test/monkey/leaflet.ie.css b/test/monkey/leaflet.ie.css new file mode 100755 index 00000000..a120c0cb --- /dev/null +++ b/test/monkey/leaflet.ie.css @@ -0,0 +1,48 @@ +.leaflet-tile { + filter: inherit; + } + +.leaflet-vml-shape { + width: 1px; + height: 1px; + } +.lvml { + behavior: url(#default#VML); + display: inline-block; + position: absolute; + } + +.leaflet-control { + display: inline; + } + +.leaflet-popup-tip { + width: 21px; + _width: 27px; + margin: 0 auto; + _margin-top: -3px; + + filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); + -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; + } +.leaflet-popup-tip-container { + margin-top: -1px; + } +.leaflet-popup-content-wrapper, .leaflet-popup-tip { + border: 1px solid #bbb; + } + +.leaflet-control-zoom { + filter: progid:DXImageTransform.Microsoft.gradient(startColorStr='#3F000000',EndColorStr='#3F000000'); + } +.leaflet-control-zoom a { + background-color: #eee; + } +.leaflet-control-zoom a:hover { + background-color: #fff; + } +.leaflet-control-layers-toggle { + } +.leaflet-control-attribution, .leaflet-control-layers { + background: white; + } \ No newline at end of file diff --git a/test/monkey/leaflet.js b/test/monkey/leaflet.js new file mode 100755 index 00000000..9dc79889 --- /dev/null +++ b/test/monkey/leaflet.js @@ -0,0 +1,127 @@ +/* + Copyright (c) 2010-2011, CloudMade, Vladimir Agafonkin + Leaflet is a BSD-licensed JavaScript library for map display and interaction. + See http://leaflet.cloudmade.com for more information. +*/ +(function(a){var b={VERSION:"0.3",ROOT_URL:function(){for(var a=document.getElementsByTagName("script"),b=/^(.*\/)leaflet-?([\w-]*)\.js.*$/,e=0,f=a.length;e0},removeEventListener:function(a,b,c){if(!this.hasEventListeners(a))return this;for(var d=0,e=this._leaflet_events,f=e[a].length;d=this.min.x&&a.x<=this.max.x&&b.y>=this.min.y&&a.y<=this.max.y}});L.Transformation=L.Class.extend({initialize:function(a,b,c,d){this._a=a;this._b=b;this._c=c;this._d=d},transform:function(a,b){return this._transform(a.clone(),b)},_transform:function(a,b){b=b||1;a.x=b*(this._a*a.x+this._b);a.y=b*(this._c*a.y+this._d);return a},untransform:function(a,b){b=b||1;return new L.Point((a.x/b-this._b)/this._a,(a.y/b-this._d)/this._c)}});L.LineUtil={simplify:function(a,b){if(!b||!a.length)return a.slice();a=this.reducePoints(a,b);return a=this.simplifyDP(a,b)},pointToSegmentDistance:function(a,b,c){return Math.sqrt(this._sqPointToSegmentDist(a,b,c))},closestPointOnSegment:function(a,b,c){a=this._sqClosestPointOnSegment(a,b,c);a.distance=Math.sqrt(a._sqDist);return a},simplifyDP:function(a,b){for(var c=0,d=0,e=b*b,f=1,g=a.length,h;fc&&(d=f,c=h);return c>=e?(c=a.slice(0,d),d= +a.slice(d),g=this.simplifyDP(c,b).slice(0,g-2),d=this.simplifyDP(d,b),g.concat(d)):[a[0],a[g-1]]},reducePoints:function(a,b){for(var c=[a[0]],d=b*b,e=1,f=0,g=a.length;eb.max.x&&(c|=2);a.yb.max.y&&(c|=8);return c},_sqDist:function(a,b){var c=b.x-a.x,d=b.y-a.y;return c*c+d*d},_sqClosestPointOnSegment:function(a, +b,c){var d=c.x-b.x,e=c.y-b.y,f=b;if(d||e){var g=((a.x-b.x)*d+(a.y-b.y)*e)/this._sqDist(b,c);g>1?f=c:g>0&&(f=new L.Point(b.x+d*g,b.y+e*g))}f._sqDist=this._sqDist(a,f);return f},_sqPointToSegmentDist:function(a,b,c){return this._sqClosestPointOnSegment(a,b,c)._sqDist}};L.PolyUtil={};L.PolyUtil.clipPolygon=function(a,b){var c,d=[1,4,2,8],e,f,g,h,j,k,l=L.LineUtil;e=0;for(j=a.length;e0&&c<=h;f=b}}function e(){if(g)j.type="dblclick",b(j),f=null}var f,g=!1,h=250,j;a["_leaflet_touchstart"+c]=d;a["_leaflet_touchend"+c]=e;a.addEventListener("touchstart",d,!1);a.addEventListener("touchend",e,!1)},removeDoubleTapListener:function(a,b){a.removeEventListener(a,a["_leaflet_touchstart"+b],!1);a.removeEventListener(a,a["_leaflet_touchend"+b], +!1)}});L.DomUtil={get:function(a){return typeof a=="string"?document.getElementById(a):a},getStyle:function(a,b){var c=a.style[b];!c&&a.currentStyle&&(c=a.currentStyle[b]);if(!c||c=="auto")c=(c=document.defaultView.getComputedStyle(a,null))?c[b]:null;return c=="auto"?null:c},getViewportOffset:function(a){var b=0,c=0,d=a,e=document.body;do if(b+=d.offsetTop||0,c+=d.offsetLeft||0,d.offsetParent==e&&L.DomUtil.getStyle(d,"position")=="absolute")break;while(d=d.offsetParent);d=a;do{if(d===e)break;b-=d.scrollTop|| +0;c-=d.scrollLeft||0}while(d=d.parentNode);return new L.Point(c,b)},create:function(a,b,c){a=document.createElement(a);a.className=b;c&&c.appendChild(a);return a},disableTextSelection:function(){document.selection&&document.selection.empty&&document.selection.empty();if(!this._onselectstart)this._onselectstart=document.onselectstart,document.onselectstart=L.Util.falseFn},enableTextSelection:function(){document.onselectstart=this._onselectstart;this._onselectstart=null},hasClass:function(a,b){return a.className.length> +0&&RegExp("(^|\\s)"+b+"(\\s|$)").test(a.className)},addClass:function(a,b){L.DomUtil.hasClass(a,b)||(a.className+=(a.className?" ":"")+b)},removeClass:function(a,b){a.className=a.className.replace(/(\S+)\s*/g,function(a,d){if(d==b)return"";return a}).replace(/^\s+/,"")},setOpacity:function(a,b){L.Browser.ie?a.style.filter="alpha(opacity="+Math.round(b*100)+")":a.style.opacity=b},testProp:function(a){for(var b=document.documentElement.style,c=0;c1)){var b=a.touches&&a.touches.length==1?a.touches[0]:a;L.DomEvent.preventDefault(a);L.Browser.touch&&(b.target.className+=" leaflet-active");this._moved=!1;L.DomUtil.disableTextSelection();this._setMovingCursor();this._startPos=this._newPos=L.DomUtil.getPosition(this._element);this._startPoint=new L.Point(b.clientX,b.clientY);L.DomEvent.addListener(document, +L.Draggable.MOVE,this._onMove,this);L.DomEvent.addListener(document,L.Draggable.END,this._onUp,this)}},_onMove:function(a){if(!(a.touches&&a.touches.length>1)){L.DomEvent.preventDefault(a);a=a.touches&&a.touches.length==1?a.touches[0]:a;if(!this._moved)this.fire("dragstart"),this._moved=!0;this._newPos=this._startPos.add(new L.Point(a.clientX,a.clientY)).subtract(this._startPoint);L.Util.requestAnimFrame(this._updatePosition,this,!0)}},_updatePosition:function(){L.DomUtil.setPosition(this._element, +this._newPos);this.fire("drag")},_onUp:function(a){if(a.changedTouches){var a=a.changedTouches[0],b=a.target,c=this._newPos&&this._newPos.distanceTo(this._startPos)||0;b.className=b.className.replace(" leaflet-active","");c=b.lat&&a.lat<=c.lat&&d.lng>=b.lng&&a.lng<=c.lng}});L.Projection={};L.Projection.SphericalMercator={MAX_LATITUDE:85.0511287798,project:function(a){var b=L.LatLng.DEG_TO_RAD,c=this.MAX_LATITUDE,d=a.lng*b,a=Math.max(Math.min(c,a.lat),-c)*b,a=Math.log(Math.tan(Math.PI/4+a/2));return new L.Point(d,a)},unproject:function(a,b){var c=L.LatLng.RAD_TO_DEG;return new L.LatLng((2*Math.atan(Math.exp(a.y))-Math.PI/2)*c,a.x*c,b)}};L.Projection.LonLat={project:function(a){return new L.Point(a.lng,a.lat)},unproject:function(a,b){return new L.LatLng(a.y,a.x,b)}};L.Projection.Mercator={MAX_LATITUDE:85.0840591556,R_MINOR:6356752.3142,R_MAJOR:6378137,project:function(a){var b=L.LatLng.DEG_TO_RAD,c=this.MAX_LATITUDE,d=this.R_MAJOR,e=a.lng*b*d,a=Math.max(Math.min(c,a.lat),-c)*b,b=this.R_MINOR/d,b=Math.sqrt(1-b*b),c=b*Math.sin(a),c=Math.pow((1-c)/(1+c),b*0.5),a=-d*Math.log(Math.tan(0.5*(Math.PI*0.5-a))/c);return new L.Point(e,a)},unproject:function(a,b){for(var c=L.LatLng.RAD_TO_DEG,d=this.R_MAJOR,e=a.x*c/d,f=this.R_MINOR/d,f=Math.sqrt(1-f*f),d=Math.exp(-a.y/d), +g=Math.PI/2-2*Math.atan(d),h=15,j=0.1;Math.abs(j)>1.0E-7&&--h>0;)j=f*Math.sin(g),j=Math.PI/2-2*Math.atan(d*Math.pow((1-j)/(1+j),0.5*f))-g,g+=j;return new L.LatLng(g*c,e,b)}};L.CRS={latLngToPoint:function(a,b){return this.transformation._transform(this.projection.project(a),b)},pointToLatLng:function(a,b,c){return this.projection.unproject(this.transformation.untransform(a,b),c)},project:function(a){return this.projection.project(a)}};L.CRS.EPSG3857=L.Util.extend({},L.CRS,{code:"EPSG:3857",projection:L.Projection.SphericalMercator,transformation:new L.Transformation(0.5/Math.PI,0.5,-0.5/Math.PI,0.5),project:function(a){return this.projection.project(a).multiplyBy(6378137)}});L.CRS.EPSG900913=L.Util.extend({},L.CRS.EPSG3857,{code:"EPSG:900913"});L.CRS.EPSG4326=L.Util.extend({},L.CRS,{code:"EPSG:4326",projection:L.Projection.LonLat,transformation:new L.Transformation(1/360,0.5,-1/360,0.5)});L.CRS.EPSG3395=L.Util.extend({},L.CRS,{code:"EPSG:3395",projection:L.Projection.Mercator,transformation:function(){var a=L.Projection.Mercator;return new L.Transformation(0.5/(Math.PI*a.R_MAJOR),0.5,-0.5/(Math.PI*a.R_MINOR),0.5)}()});L.LayerGroup=L.Class.extend({initialize:function(a){this._layers={};if(a)for(var b=0,c=a.length;ba.max.x||ba.max.y))this._tiles[d].parentNode==this._container&&this._container.removeChild(this._tiles[d]),delete this._tiles[d]},_addTile:function(a,b){var c=this._getTilePos(a),d=this._map.getZoom(),e=a.x+":"+a.y,f=1<=f){this._tilesToLoad--;return}}else a.x=(a.x%f+f)%f;if(a.y<0||a.y>=f){this._tilesToLoad--;return}}var g=this._createTile();L.DomUtil.setPosition(g, +c);this._tiles[e]=g;if(this.options.scheme=="tms")a.y=f-a.y-1;this._loadTile(g,a,d);b.appendChild(g)},_getTilePos:function(a){var b=this._map.getPixelOrigin();return a.multiplyBy(this.options.tileSize).subtract(b)},getTileUrl:function(a,b){return this._url.replace("{s}",this.options.subdomains[(a.x+a.y)%this.options.subdomains.length]).replace("{z}",b).replace("{x}",a.x).replace("{y}",a.y)},_createTileProto:function(){this._tileImg=L.DomUtil.create("img","leaflet-tile");this._tileImg.galleryimg="no"; +var a=this.options.tileSize;this._tileImg.style.width=a+"px";this._tileImg.style.height=a+"px"},_createTile:function(){var a=this._tileImg.cloneNode(!1);a.onselectstart=a.onmousemove=L.Util.falseFn;return a},_loadTile:function(a,b,c){a._layer=this;a.onload=this._tileOnLoad;a.onerror=this._tileOnError;a.src=this.getTileUrl(b,c)},_tileOnLoad:function(){var a=this._layer;this.className+=" leaflet-tile-loaded";a.fire("tileload",{tile:this,url:this.src});a._tilesToLoad--;a._tilesToLoad||a.fire("load")}, +_tileOnError:function(){var a=this._layer;a.fire("tileerror",{tile:this,url:this.src});if(a=a.options.errorTileUrl)this.src=a}});L.TileLayer.WMS=L.TileLayer.extend({defaultWmsParams:{service:"WMS",request:"GetMap",version:"1.1.1",layers:"",styles:"",format:"image/jpeg",transparent:!1},initialize:function(a,b){this._url=a;this.wmsParams=L.Util.extend({},this.defaultWmsParams);this.wmsParams.width=this.wmsParams.height=this.options.tileSize;for(var c in b)this.options.hasOwnProperty(c)||(this.wmsParams[c]=b[c]);L.Util.setOptions(this,b)},onAdd:function(a){this.wmsParams[parseFloat(this.wmsParams.version)>=1.3?"crs":"srs"]=a.options.crs.code; +L.TileLayer.prototype.onAdd.call(this,a)},getTileUrl:function(a){var b=this.options.tileSize,a=a.multiplyBy(b),b=a.add(new L.Point(b,b)),a=this._map.unproject(a,this._zoom,!0),b=this._map.unproject(b,this._zoom,!0),a=this._map.options.crs.project(a),b=this._map.options.crs.project(b),b=[a.x,b.y,b.x,a.y].join(",");return this._url+L.Util.getParamString(this.wmsParams)+"&bbox="+b}});L.TileLayer.Canvas=L.TileLayer.extend({options:{async:!1},initialize:function(a){L.Util.setOptions(this,a)},_createTileProto:function(){this._canvasProto=L.DomUtil.create("canvas","leaflet-tile");var a=this.options.tileSize;this._canvasProto.width=a;this._canvasProto.height=a},_createTile:function(){var a=this._canvasProto.cloneNode(!1);a.onselectstart=a.onmousemove=L.Util.falseFn;return a},_loadTile:function(a,b,c){a._layer=this;this.drawTile(a,b,c);this.options.async||this.tileDrawn(a)},drawTile:function(){}, +tileDrawn:function(a){this._tileOnLoad.call(a)}});L.ImageOverlay=L.Class.extend({includes:L.Mixin.Events,initialize:function(a,b){this._url=a;this._bounds=b},onAdd:function(a){this._map=a;this._image||this._initImage();a.getPanes().overlayPane.appendChild(this._image);a.on("viewreset",this._reset,this);this._reset()},onRemove:function(a){a.getPanes().overlayPane.removeChild(this._image);a.off("viewreset",this._reset,this)},_initImage:function(){this._image=L.DomUtil.create("img","leaflet-image-layer");this._image.style.visibility="hidden";L.Util.extend(this._image, +{galleryimg:"no",onselectstart:L.Util.falseFn,onmousemove:L.Util.falseFn,onload:this._onImageLoad,src:this._url})},_reset:function(){var a=this._map.latLngToLayerPoint(this._bounds.getNorthWest()),b=this._map.latLngToLayerPoint(this._bounds.getSouthEast()).subtract(a);L.DomUtil.setPosition(this._image,a);this._image.style.width=b.x+"px";this._image.style.height=b.y+"px"},_onImageLoad:function(){this.style.visibility=""}});L.Popup=L.Class.extend({includes:L.Mixin.Events,options:{minWidth:50,maxWidth:300,autoPan:!0,closeButton:!0,offset:new L.Point(0,2),autoPanPadding:new L.Point(5,5)},initialize:function(a){L.Util.setOptions(this,a)},onAdd:function(a){this._map=a;this._container||this._initLayout();this._updateContent();this._container.style.opacity="0";this._map._panes.popupPane.appendChild(this._container);this._map.on("viewreset",this._updatePosition,this);if(this._map.options.closePopupOnClick)this._map.on("preclick", +this._close,this);this._update();this._container.style.opacity="1";this._opened=!0},onRemove:function(a){a._panes.popupPane.removeChild(this._container);a.off("viewreset",this._updatePosition,this);a.off("click",this._close,this);this._container.style.opacity="0";this._opened=!1},setLatLng:function(a){this._latlng=a;this._opened&&this._update();return this},setContent:function(a){this._content=a;this._opened&&this._update();return this},_close:function(){this._opened&&this._map.removeLayer(this)}, +_initLayout:function(){this._container=L.DomUtil.create("div","leaflet-popup");if(this.options.closeButton)this._closeButton=L.DomUtil.create("a","leaflet-popup-close-button",this._container),this._closeButton.href="#close",L.DomEvent.addListener(this._closeButton,"click",this._onCloseButtonClick,this);this._wrapper=L.DomUtil.create("div","leaflet-popup-content-wrapper",this._container);L.DomEvent.disableClickPropagation(this._wrapper);this._contentNode=L.DomUtil.create("div","leaflet-popup-content", +this._wrapper);this._tipContainer=L.DomUtil.create("div","leaflet-popup-tip-container",this._container);this._tip=L.DomUtil.create("div","leaflet-popup-tip",this._tipContainer)},_update:function(){this._container.style.visibility="hidden";this._updateContent();this._updateLayout();this._updatePosition();this._container.style.visibility="";this._adjustPan()},_updateContent:function(){if(this._content)typeof this._content=="string"?this._contentNode.innerHTML=this._content:(this._contentNode.innerHTML= +"",this._contentNode.appendChild(this._content))},_updateLayout:function(){this._container.style.width="";this._container.style.whiteSpace="nowrap";var a=this._container.offsetWidth;this._container.style.width=(a>this.options.maxWidth?this.options.maxWidth:ae.x)c.x=b.x+ +this._containerWidth-e.x+d.x;if(b.y<0)c.y=b.y-d.y;if(b.y+a>e.y)c.y=b.y+a-e.y+d.y;(c.x||c.y)&&this._map.panBy(c)}},_onCloseButtonClick:function(a){this._close();L.DomEvent.stop(a)}});L.Icon=L.Class.extend({iconUrl:L.ROOT_URL+"images/marker.png",shadowUrl:L.ROOT_URL+"images/marker-shadow.png",iconSize:new L.Point(25,41),shadowSize:new L.Point(41,41),iconAnchor:new L.Point(13,41),popupAnchor:new L.Point(0,-33),initialize:function(a){if(a)this.iconUrl=a},createIcon:function(){return this._createIcon("icon")},createShadow:function(){return this._createIcon("shadow")},_createIcon:function(a){var b=this[a+"Size"],c=this[a+"Url"],d=this._createImg(c);if(!c)return null;d.className="leaflet-marker-"+ +a;d.style.marginLeft=-this.iconAnchor.x+"px";d.style.marginTop=-this.iconAnchor.y+"px";if(b)d.style.width=b.x+"px",d.style.height=b.y+"px";return d},_createImg:function(a){var b;L.Browser.ie6?(b=document.createElement("div"),b.style.filter='progid:DXImageTransform.Microsoft.AlphaImageLoader(src="'+a+'")'):(b=document.createElement("img"),b.src=a);return b}});L.Marker=L.Class.extend({includes:L.Mixin.Events,options:{icon:new L.Icon,title:"",clickable:!0,draggable:!1},initialize:function(a,b){L.Util.setOptions(this,b);this._latlng=a},onAdd:function(a){this._map=a;this._initIcon();a.on("viewreset",this._reset,this);this._reset()},onRemove:function(a){this._removeIcon();this.closePopup&&this.closePopup();a.off("viewreset",this._reset,this)},getLatLng:function(){return this._latlng},setLatLng:function(a){this._latlng=a;this._icon&&this._reset()},setIcon:function(a){this._removeIcon(); +this._icon=this._shadow=null;this.options.icon=a;this._initIcon();this._reset()},_initIcon:function(){if(!this._icon){this._icon=this.options.icon.createIcon();if(this.options.title)this._icon.title=this.options.title;this._initInteraction()}if(!this._shadow)this._shadow=this.options.icon.createShadow();this._map._panes.markerPane.appendChild(this._icon);this._shadow&&this._map._panes.shadowPane.appendChild(this._shadow)},_removeIcon:function(){this._map._panes.markerPane.removeChild(this._icon); +this._shadow&&this._map._panes.shadowPane.removeChild(this._shadow)},_reset:function(){var a=this._map.latLngToLayerPoint(this._latlng).round();L.DomUtil.setPosition(this._icon,a);this._shadow&&L.DomUtil.setPosition(this._shadow,a);this._icon.style.zIndex=a.y},_initInteraction:function(){if(this.options.clickable){this._icon.className+=" leaflet-clickable";L.DomEvent.addListener(this._icon,"click",this._onMouseClick,this);for(var a=["dblclick","mousedown","mouseover","mouseout"],b=0;b';a=a.firstChild;a.style.behavior="url(#default#VML)";return a&&typeof a.adj=="object"}(); +L.Path=L.Browser.svg||!L.Browser.vml?L.Path:L.Path.extend({statics:{VML:!0,CLIP_PADDING:0.02},_createElement:function(){try{return document.namespaces.add("lvml","urn:schemas-microsoft-com:vml"),function(a){return document.createElement("')}}catch(a){return function(a){return document.createElement("<"+a+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}}(),_initRoot:function(){if(!this._map._pathRoot)this._map._pathRoot=document.createElement("div"),this._map._pathRoot.className= +"leaflet-vml-container",this._map._panes.overlayPane.appendChild(this._map._pathRoot),this._map.on("moveend",this._updateViewport,this),this._updateViewport()},_initPath:function(){this._container=this._createElement("shape");this._container.className+=" leaflet-vml-shape"+(this.options.clickable?" leaflet-clickable":"");this._container.coordsize="1 1";this._path=this._createElement("path");this._container.appendChild(this._path);this._map._pathRoot.appendChild(this._container)},_initStyle:function(){this.options.stroke? +(this._stroke=this._createElement("stroke"),this._stroke.endcap="round",this._container.appendChild(this._stroke)):this._container.stroked=!1;this.options.fill?(this._container.filled=!0,this._fill=this._createElement("fill"),this._container.appendChild(this._fill)):this._container.filled=!1;this._updateStyle()},_updateStyle:function(){if(this.options.stroke)this._stroke.weight=this.options.weight+"px",this._stroke.color=this.options.color,this._stroke.opacity=this.options.opacity;if(this.options.fill)this._fill.color= +this.options.fillColor||this.options.color,this._fill.opacity=this.options.fillOpacity},_updatePath:function(){this._container.style.display="none";this._path.v=this.getPathString()+" ";this._container.style.display=""}});L.Browser.canvas=function(){return!!document.createElement("canvas").getContext}(); +L.Path=L.Path.SVG&&!window.L_PREFER_CANVAS||!L.Browser.canvas?L.Path:L.Path.extend({statics:{CANVAS:!0,SVG:!1},options:{updateOnMoveEnd:!0},_initElements:function(){this._initRoot()},_initRoot:function(){var a=this._map._pathRoot,b=this._map._canvasCtx;if(!a)a=this._map._pathRoot=document.createElement("canvas"),a.style.position="absolute",b=this._map._canvasCtx=a.getContext("2d"),b.lineCap="round",b.lineJoin="round",this._map._panes.overlayPane.appendChild(a),this._map.on("moveend",this._updateCanvasViewport, +this),this._updateCanvasViewport();this._ctx=b},_updateStyle:function(){if(this.options.stroke)this._ctx.lineWidth=this.options.weight,this._ctx.strokeStyle=this.options.color;if(this.options.fill)this._ctx.fillStyle=this.options.fillColor||this.options.color},_drawPath:function(){var a,b,c,d,e,f;this._ctx.beginPath();a=0;for(c=this._parts.length;aa.y!=e.y>a.y&&a.x<(e.x-d.x)*(a.y-d.y)/(e.y-d.y)+d.x&&(b=!b)}return b}});(function(){function a(a){return L.FeatureGroup.extend({initialize:function(a,b){this._layers={};this._options=b;this.setLatLngs(a)},setStyle:function(a){for(var b in this._layers)this._layers.hasOwnProperty(b)&&this._layers[b].setStyle&&this._layers[b].setStyle(a)},setLatLngs:function(c){var d=0,e=c.length;for(this._iterateLayers(function(a){da.max.x||c.y-b>a.max.y||c.x+b0?Math.ceil(b):Math.floor(b))),a=b-a,c=this._centerOffset.subtract(this._delta).divideBy(this._scale),d=this._map.unproject(this._map.getPixelOrigin().add(this._startCenter).add(c));L.DomEvent.removeListener(document,"touchmove",this._onTouchMove);L.DomEvent.removeListener(document,"touchend",this._onTouchEnd);this._map._runAnimation(d,b,Math.pow(2,a)/this._scale,this._startCenter.add(c))}}});L.Handler.ScrollWheelZoom=L.Handler.extend({enable:function(){if(!this._enabled)L.DomEvent.addListener(this._map._container,"mousewheel",this._onWheelScroll,this),this._delta=0,this._enabled=!0},disable:function(){if(this._enabled)L.DomEvent.removeListener(this._map._container,"mousewheel",this._onWheelScroll),this._enabled=!1},_onWheelScroll:function(a){this._delta+=L.DomEvent.getWheelDelta(a);this._lastMousePos=this._map.mouseEventToContainerPoint(a);clearTimeout(this._timer);this._timer=setTimeout(L.Util.bind(this._performZoom, +this),50);L.DomEvent.preventDefault(a)},_performZoom:function(){var a=Math.round(this._delta),a=Math.max(Math.min(a,4),-4);this._delta=0;if(a){var b=this._getCenterForScrollWheelZoom(this._lastMousePos,a),a=this._map.getZoom()+a;this._map._limitZoom(a)!=this._map._zoom&&this._map.setView(b,a)}},_getCenterForScrollWheelZoom:function(a,b){var c=this._map.getPixelBounds().getCenter(),d=this._map.getSize().divideBy(2),d=a.subtract(d).multiplyBy(1-Math.pow(2,-b));return this._map.unproject(c.add(d),this._map._zoom, +!0)}});L.Handler.DoubleClickZoom=L.Handler.extend({enable:function(){if(!this._enabled)this._map.on("dblclick",this._onDoubleClick,this._map),this._enabled=!0},disable:function(){if(this._enabled)this._map.off("dblclick",this._onDoubleClick,this._map),this._enabled=!1},_onDoubleClick:function(a){this.setView(a.latlng,this._zoom+1)}});L.Handler.ShiftDragZoom=L.Handler.extend({initialize:function(a){this._map=a;this._container=a._container;this._pane=a._panes.overlayPane},enable:function(){if(!this._enabled)L.DomEvent.addListener(this._container,"mousedown",this._onMouseDown,this),this._enabled=!0},disable:function(){if(this._enabled)L.DomEvent.removeListener(this._container,"mousedown",this._onMouseDown),this._enabled=!1},_onMouseDown:function(a){if(!a.shiftKey||a.which!=1&&a.button!=1)return!1;L.DomUtil.disableTextSelection(); +this._startLayerPoint=this._map.mouseEventToLayerPoint(a);this._box=L.DomUtil.create("div","leaflet-zoom-box",this._pane);L.DomUtil.setPosition(this._box,this._startLayerPoint);this._container.style.cursor="crosshair";L.DomEvent.addListener(document,"mousemove",this._onMouseMove,this);L.DomEvent.addListener(document,"mouseup",this._onMouseUp,this);L.DomEvent.preventDefault(a)},_onMouseMove:function(a){var b=this._map.mouseEventToLayerPoint(a),a=b.x-this._startLayerPoint.x,c=b.y-this._startLayerPoint.y, +b=new L.Point(Math.min(b.x,this._startLayerPoint.x),Math.min(b.y,this._startLayerPoint.y));L.DomUtil.setPosition(this._box,b);this._box.style.width=Math.abs(a)-4+"px";this._box.style.height=Math.abs(c)-4+"px"},_onMouseUp:function(a){this._pane.removeChild(this._box);this._container.style.cursor="";L.DomUtil.enableTextSelection();L.DomEvent.removeListener(document,"mousemove",this._onMouseMove);L.DomEvent.removeListener(document,"mouseup",this._onMouseUp);a=this._map.mouseEventToLayerPoint(a);this._map.fitBounds(new L.LatLngBounds(this._map.layerPointToLatLng(this._startLayerPoint), +this._map.layerPointToLatLng(a)))}});L.Handler.MarkerDrag=L.Handler.extend({initialize:function(a){this._marker=a},enable:function(){if(!this._enabled){if(!this._draggable)this._draggable=new L.Draggable(this._marker._icon,this._marker._icon),this._draggable.on("dragstart",this._onDragStart,this),this._draggable.on("drag",this._onDrag,this),this._draggable.on("dragend",this._onDragEnd,this);this._draggable.enable();this._enabled=!0}},disable:function(){if(this._enabled)this._draggable.disable(),this._enabled=!1},moved:function(){return this._draggable&& +this._draggable._moved},_onDragStart:function(){this._marker.closePopup();this._marker.fire("movestart");this._marker.fire("dragstart")},_onDrag:function(){var a=L.DomUtil.getPosition(this._marker._icon);this._marker._shadow&&L.DomUtil.setPosition(this._marker._shadow,a);this._marker._latlng=this._marker._map.layerPointToLatLng(a);this._marker.fire("move");this._marker.fire("drag")},_onDragEnd:function(){this._marker.fire("moveend");this._marker.fire("dragend")}});L.Control={};L.Control.Position={TOP_LEFT:"topLeft",TOP_RIGHT:"topRight",BOTTOM_LEFT:"bottomLeft",BOTTOM_RIGHT:"bottomRight"};L.Control.Zoom=L.Class.extend({onAdd:function(a){this._map=a;this._container=L.DomUtil.create("div","leaflet-control-zoom");this._zoomInButton=this._createButton("Zoom in","leaflet-control-zoom-in",this._map.zoomIn,this._map);this._zoomOutButton=this._createButton("Zoom out","leaflet-control-zoom-out",this._map.zoomOut,this._map);this._container.appendChild(this._zoomInButton);this._container.appendChild(this._zoomOutButton)},getContainer:function(){return this._container},getPosition:function(){return L.Control.Position.TOP_LEFT}, +_createButton:function(a,b,c,d){var e=document.createElement("a");e.href="#";e.title=a;e.className=b;L.DomEvent.disableClickPropagation(e);L.DomEvent.addListener(e,"click",L.DomEvent.preventDefault);L.DomEvent.addListener(e,"click",c,d);return e}});L.Control.Attribution=L.Class.extend({onAdd:function(a){this._container=L.DomUtil.create("div","leaflet-control-attribution");this._map=a;this._prefix='Powered by Leaflet';this._attributions={};this._update()},getPosition:function(){return L.Control.Position.BOTTOM_RIGHT},getContainer:function(){return this._container},setPrefix:function(a){this._prefix=a;this._update()},addAttribution:function(a){a&&(this._attributions[a]=!0,this._update())},removeAttribution:function(a){a&& +(delete this._attributions[a],this._update())},_update:function(){if(this._map){var a=[],b;for(b in this._attributions)this._attributions.hasOwnProperty(b)&&a.push(b);b=[];this._prefix&&b.push(this._prefix);a.length&&b.push(a.join(", "));this._container.innerHTML=b.join(" — ")}}});L.Control.Layers=L.Class.extend({options:{collapsed:!0},initialize:function(a,b,c){L.Util.setOptions(this,c);this._layers={};for(var d in a)a.hasOwnProperty(d)&&this._addLayer(a[d],d);for(d in b)b.hasOwnProperty(d)&&this._addLayer(b[d],d,!0)},onAdd:function(a){this._map=a;this._initLayout();this._update()},getContainer:function(){return this._container},getPosition:function(){return L.Control.Position.TOP_RIGHT},addBaseLayer:function(a,b){this._addLayer(a,b);this._update();return this},addOverlay:function(a, +b){this._addLayer(a,b,!0);this._update();return this},removeLayer:function(a){delete this._layers[L.Util.stamp(a)];this._update();return this},_initLayout:function(){this._container=L.DomUtil.create("div","leaflet-control-layers");L.DomEvent.disableClickPropagation(this._container);this._form=L.DomUtil.create("form","leaflet-control-layers-list");if(this.options.collapsed){L.DomEvent.addListener(this._container,"mouseover",this._expand,this);L.DomEvent.addListener(this._container,"mouseout",this._collapse, +this);var a=this._layersLink=L.DomUtil.create("a","leaflet-control-layers-toggle");a.href="#";a.title="Layers";L.DomEvent.addListener(a,"focus",this._expand,this);L.DomEvent.addListener(this._map,L.Draggable.START,this._collapse,this);this._container.appendChild(a)}else this._expand();this._baseLayersList=L.DomUtil.create("div","leaflet-control-layers-base",this._form);this._separator=L.DomUtil.create("div","leaflet-control-layers-separator",this._form);this._overlaysList=L.DomUtil.create("div","leaflet-control-layers-overlays", +this._form);this._container.appendChild(this._form)},_addLayer:function(a,b,c){this._layers[L.Util.stamp(a)]={layer:a,name:b,overlay:c}},_update:function(){if(this._container){this._baseLayersList.innerHTML="";this._overlaysList.innerHTML="";var a=!1,b=!1,c;for(c in this._layers)if(this._layers.hasOwnProperty(c)){var d=this._layers[c];this._addItem(d);b=b||d.overlay;a=a||!d.overlay}this._separator.style.display=b&&a?"":"none"}},_addItem:function(a){var b=document.createElement("label"),c=document.createElement("input"); +if(!a.overlay)c.name="leaflet-base-layers";c.type=a.overlay?"checkbox":"radio";c.checked=this._map.hasLayer(a.layer);c.layerId=L.Util.stamp(a.layer);L.DomEvent.addListener(c,"click",this._onInputClick,this);var d=document.createTextNode(" "+a.name);b.appendChild(c);b.appendChild(d);(a.overlay?this._overlaysList:this._baseLayersList).appendChild(b)},_onInputClick:function(){var a,b,c,d=this._form.getElementsByTagName("input"),e=d.length;for(a=0;a