diff --git a/leaflet.wms.js b/leaflet.wms.js index ea2f071..7d058bc 100644 --- a/leaflet.wms.js +++ b/leaflet.wms.js @@ -25,6 +25,24 @@ // Module object var wms = {}; +// Hacky backport of L.Layer for 0.7.3 +if (!L.Layer) { + L.Layer = L.LayerGroup.extend({ + 'onAdd': function(map) { + this._map = map; + if (map && this.getEvents) { + var events = this.getEvents(); + for (var evt in events) { + map.on(evt, events[evt], this); + } + } + }, + 'onRemove': function() { + delete this._map; + } + }); +} + /* * wms.Source * The Source object manages a single WMS connection. Multiple "layers" can be @@ -53,7 +71,8 @@ wms.Source = L.Layer.extend({ } }, - 'onAdd': function() { + 'onAdd': function(map) { + L.Layer.prototype.onAdd.call(this, map); this.refreshOverlay(); }, @@ -85,7 +104,7 @@ wms.Source = L.Layer.extend({ return; } if (!subLayers) { - this._overlay.remove(); + this._map.removeLayer(this._overlay); } else { this._overlay.setParams({'layers': subLayers}); this._overlay.addTo(this._map); @@ -207,7 +226,8 @@ wms.Layer = L.Layer.extend({ this._source = source; this._name = layerName; }, - 'onAdd': function() { + 'onAdd': function(map) { + L.Layer.prototype.onAdd.call(this, map); if (!this._source._map) this._source.addTo(this._map); this._source.addSubLayer(this._name); @@ -284,7 +304,8 @@ wms.Overlay = L.Layer.extend({ return this.options.attribution; }, - 'onAdd': function() { + 'onAdd': function(map) { + L.Layer.prototype.onAdd.call(this, map); this.update(); }, @@ -296,6 +317,7 @@ wms.Overlay = L.Layer.extend({ if (this._currentUrl) { delete this._currentUrl; } + L.Layer.prototype.onRemove.call(this, map); }, 'getEvents': function() { diff --git a/lib/leaflet.js b/lib/leaflet.js index bc30055..640ef73 100644 --- a/lib/leaflet.js +++ b/lib/leaflet.js @@ -1,22 +1,13 @@ /* - Leaflet 0.8-dev (ae6d259), a JS library for interactive maps. http://leafletjs.com - (c) 2010-2014 Vladimir Agafonkin, (c) 2010-2011 CloudMade + Leaflet, a JavaScript library for mobile-friendly interactive maps. http://leafletjs.com + (c) 2010-2013, Vladimir Agafonkin + (c) 2010-2011, CloudMade */ (function (window, document, undefined) { -var L = { - version: '0.8-dev' -}; - -function expose() { - var oldL = window.L; - - L.noConflict = function () { - window.L = oldL; - return this; - }; +var oldL = window.L, + L = {}; - window.L = L; -} +L.version = '0.7.3'; // define Leaflet for Node module pattern loaders, including Browserify if (typeof module === 'object' && typeof module.exports === 'object') { @@ -25,11 +16,16 @@ if (typeof module === 'object' && typeof module.exports === 'object') { // define Leaflet as an AMD module } else if (typeof define === 'function' && define.amd) { define(L); +} // define Leaflet as a global L variable, saving the original L to restore later if needed -} else { - expose(); -} + +L.noConflict = function () { + window.L = oldL; + return this; +}; + +window.L = L; /* @@ -37,120 +33,100 @@ if (typeof module === 'object' && typeof module.exports === 'object') { */ L.Util = { - // extend an object with properties of one or more other objects - extend: function (dest) { - var i, j, len, src; + extend: function (dest) { // (Object[, Object, ...]) -> + var sources = Array.prototype.slice.call(arguments, 1), + i, j, len, src; - for (j = 1, len = arguments.length; j < len; j++) { - src = arguments[j]; + for (j = 0, len = sources.length; j < len; j++) { + src = sources[j] || {}; for (i in src) { - dest[i] = src[i]; + if (src.hasOwnProperty(i)) { + dest[i] = src[i]; + } } } return dest; }, - // create an object from a given prototype - create: Object.create || (function () { - function F() {} - return function (proto) { - F.prototype = proto; - return new F(); + bind: function (fn, obj) { // (Function, Object) -> Function + var args = arguments.length > 2 ? Array.prototype.slice.call(arguments, 2) : null; + return function () { + return fn.apply(obj, args || arguments); }; - })(), + }, - // bind a function to be called with a given context - bind: function (fn, obj) { - var slice = Array.prototype.slice; + stamp: (function () { + var lastId = 0, + key = '_leaflet_id'; + return function (obj) { + obj[key] = obj[key] || ++lastId; + return obj[key]; + }; + }()), - if (fn.bind) { - return fn.bind.apply(fn, slice.call(arguments, 1)); - } + invokeEach: function (obj, method, context) { + var i, args; - var args = slice.call(arguments, 2); + if (typeof obj === 'object') { + args = Array.prototype.slice.call(arguments, 3); - return function () { - return fn.apply(obj, args.length ? args.concat(slice.call(arguments)) : arguments); - }; - }, + for (i in obj) { + method.apply(context, [i, obj[i]].concat(args)); + } + return true; + } - // return unique ID of an object - stamp: function (obj) { - // jshint camelcase: false - obj._leaflet_id = obj._leaflet_id || ++L.Util.lastId; - return obj._leaflet_id; + return false; }, - lastId: 0, + limitExecByInterval: function (fn, time, context) { + var lock, execOnUnlock; - // return a function that won't be called more often than the given interval - throttle: function (fn, time, context) { - var lock, args, wrapperFn, later; + return function wrapperFn() { + var args = arguments; - later = function () { - // reset lock and call if queued - lock = false; - if (args) { - wrapperFn.apply(context, args); - args = false; + if (lock) { + execOnUnlock = true; + return; } - }; - wrapperFn = function () { - if (lock) { - // called too soon, queue to call later - args = arguments; + lock = true; - } else { - // call and lock until later - fn.apply(context, arguments); - setTimeout(later, time); - lock = true; - } - }; + setTimeout(function () { + lock = false; - return wrapperFn; - }, + if (execOnUnlock) { + wrapperFn.apply(context, args); + execOnUnlock = false; + } + }, time); - // wrap the given number to lie within a certain range (used for wrapping longitude) - wrapNum: function (x, range, includeMax) { - var max = range[1], - min = range[0], - d = max - min; - return x === max && includeMax ? x : ((x - min) % d + d) % d + min; + fn.apply(context, args); + }; }, - // do nothing (used as a noop throughout the code) - falseFn: function () { return false; }, + falseFn: function () { + return false; + }, - // round a given number to a given precision formatNum: function (num, digits) { var pow = Math.pow(10, digits || 5); return Math.round(num * pow) / pow; }, - // trim whitespace from both sides of a string trim: function (str) { return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, ''); }, - // split a string into words splitWords: function (str) { return L.Util.trim(str).split(/\s+/); }, - // set options to an object, inheriting parent's options as well setOptions: function (obj, options) { - if (!obj.hasOwnProperty('options')) { - obj.options = obj.options ? L.Util.create(obj.options) : {}; - } - for (var i in options) { - obj.options[i] = options[i]; - } + obj.options = L.extend({}, obj.options, options); return obj.options; }, - // make an URL with GET parameters out of a set of properties/values getParamString: function (obj, existingUrl, uppercase) { var params = []; for (var i in obj) { @@ -158,15 +134,11 @@ L.Util = { } return ((!existingUrl || existingUrl.indexOf('?') === -1) ? '?' : '&') + params.join('&'); }, - - // super-simple templating facility, used for TileLayer URLs template: function (str, data) { - return str.replace(L.Util.templateRe, function (str, key) { + 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); } @@ -174,26 +146,30 @@ L.Util = { }); }, - templateRe: /\{ *([\w_]+) *\}/g, - isArray: Array.isArray || function (obj) { return (Object.prototype.toString.call(obj) === '[object Array]'); }, - // minimal image URI, set to an image when disposing to flush memory emptyImageUrl: 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=' }; (function () { + // inspired by http://paulirish.com/2011/requestanimationframe-for-smart-animating/ function getPrefixed(name) { - return window['webkit' + name] || window['moz' + name] || window['ms' + name]; + var i, fn, + prefixes = ['webkit', 'moz', 'o', 'ms']; + + for (i = 0; i < prefixes.length && !fn; i++) { + fn = window[prefixes[i] + name]; + } + + return fn; } var lastTime = 0; - // fallback for IE 7-8 function timeoutDefer(fn) { var time = +new Date(), timeToCall = Math.max(0, 16 - (time - lastTime)); @@ -202,16 +178,22 @@ L.Util = { return window.setTimeout(fn, timeToCall); } - var requestFn = window.requestAnimationFrame || getPrefixed('RequestAnimationFrame') || timeoutDefer, - cancelFn = window.cancelAnimationFrame || getPrefixed('CancelAnimationFrame') || - getPrefixed('CancelRequestAnimationFrame') || function (id) { window.clearTimeout(id); }; + var requestFn = window.requestAnimationFrame || + getPrefixed('RequestAnimationFrame') || timeoutDefer; + + var cancelFn = window.cancelAnimationFrame || + getPrefixed('CancelAnimationFrame') || + getPrefixed('CancelRequestAnimationFrame') || + function (id) { window.clearTimeout(id); }; - L.Util.requestAnimFrame = function (fn, context, immediate) { + L.Util.requestAnimFrame = function (fn, context, immediate, element) { + fn = L.bind(fn, context); + if (immediate && requestFn === timeoutDefer) { - fn.call(context); + fn(); } else { - return requestFn.call(window, L.bind(fn, context)); + return requestFn.call(window, fn, element); } }; @@ -220,7 +202,8 @@ L.Util = { cancelFn.call(window, id); } }; -})(); + +}()); // shortcuts for most used utility functions L.extend = L.Util.extend; @@ -247,15 +230,16 @@ L.Class.extend = function (props) { } // call all constructor hooks - if (this._initHooks.length) { + if (this._initHooks) { this.callInitHooks(); } }; - // jshint camelcase: false - var parentProto = NewClass.__super__ = this.prototype; + // instantiate class without calling constructor + var F = function () {}; + F.prototype = this.prototype; - var proto = L.Util.create(parentProto); + var proto = new F(); proto.constructor = NewClass; NewClass.prototype = proto; @@ -280,8 +264,8 @@ L.Class.extend = function (props) { } // merge options - if (proto.options) { - props.options = L.Util.extend(L.Util.create(proto.options), props.options); + if (props.options && proto.options) { + props.options = L.extend({}, proto.options, props.options); } // mix given properties into the prototype @@ -289,13 +273,17 @@ L.Class.extend = function (props) { proto._initHooks = []; + var parent = this; + // jshint camelcase: false + NewClass.__super__ = parent.prototype; + // add method for calling all hooks proto.callInitHooks = function () { if (this._initHooksCalled) { return; } - if (parentProto.callInitHooks) { - parentProto.callInitHooks.call(this); + if (parent.prototype.callInitHooks) { + parent.prototype.callInitHooks.call(this); } this._initHooksCalled = true; @@ -333,233 +321,183 @@ L.Class.addInitHook = function (fn) { // (Function) || (String, args...) /* - * L.Evented is a base class that Leaflet classes inherit from to handle custom events. + * L.Mixin.Events is used to add custom events functionality to Leaflet classes. */ -L.Evented = L.Class.extend({ +var eventsKey = '_leaflet_events'; + +L.Mixin = {}; + +L.Mixin.Events = { - on: function (types, fn, context) { + addEventListener: function (types, fn, context) { // (String, Function[, Object]) or (Object[, Object]) // types can be a map of types/handlers - if (typeof types === 'object') { - for (var type in types) { - // we don't process space-separated events here for performance; - // it's a hot path since Layer uses the on(obj) syntax - this._on(type, types[type], fn); - } + if (L.Util.invokeEach(types, this.addEventListener, this, fn, context)) { return this; } - } else { - // types can be a string of space-separated words - types = L.Util.splitWords(types); + var events = this[eventsKey] = this[eventsKey] || {}, + contextId = context && context !== this && L.stamp(context), + i, len, event, type, indexKey, indexLenKey, typeIndex; - for (var i = 0, len = types.length; i < len; i++) { - this._on(types[i], fn, context); - } - } + // types can be a string of space-separated words + types = L.Util.splitWords(types); - return this; - }, + for (i = 0, len = types.length; i < len; i++) { + event = { + action: fn, + context: context || this + }; + type = types[i]; - off: function (types, fn, context) { + if (contextId) { + // store listeners of a particular context in a separate hash (if it has an id) + // gives a major performance boost when removing thousands of map layers - if (!types) { - // clear all listeners if called without arguments - delete this._events; + indexKey = type + '_idx'; + indexLenKey = indexKey + '_len'; - } else if (typeof types === 'object') { - for (var type in types) { - this._off(type, types[type], fn); - } + typeIndex = events[indexKey] = events[indexKey] || {}; - } else { - types = L.Util.splitWords(types); + if (!typeIndex[contextId]) { + typeIndex[contextId] = []; + + // keep track of the number of keys in the index to quickly check if it's empty + events[indexLenKey] = (events[indexLenKey] || 0) + 1; + } + + typeIndex[contextId].push(event); - for (var i = 0, len = types.length; i < len; i++) { - this._off(types[i], fn, context); + + } else { + events[type] = events[type] || []; + events[type].push(event); } } return this; }, - // attach listener (without syntactic sugar now) - _on: function (type, fn, context) { - - var events = this._events = this._events || {}, - contextId = context && context !== this && L.stamp(context); - - if (contextId) { - // store listeners with custom context in a separate hash (if it has an id); - // gives a major performance boost when firing and removing events (e.g. on map object) - - var indexKey = type + '_idx', - indexLenKey = type + '_len', - typeIndex = events[indexKey] = events[indexKey] || {}, - id = L.stamp(fn) + '_' + contextId; - - if (!typeIndex[id]) { - typeIndex[id] = {fn: fn, ctx: context}; + hasEventListeners: function (type) { // (String) -> Boolean + var events = this[eventsKey]; + return !!events && ((type in events && events[type].length > 0) || + (type + '_idx' in events && events[type + '_idx_len'] > 0)); + }, - // keep track of the number of keys in the index to quickly check if it's empty - events[indexLenKey] = (events[indexLenKey] || 0) + 1; - } + removeEventListener: function (types, fn, context) { // ([String, Function, Object]) or (Object[, Object]) - } else { - // individual layers mostly use "this" for context and don't fire listeners too often - // so simple array makes the memory footprint better while not degrading performance + if (!this[eventsKey]) { + return this; + } - events[type] = events[type] || []; - events[type].push({fn: fn}); + if (!types) { + return this.clearAllEventListeners(); } - }, - _off: function (type, fn, context) { - var events = this._events, - indexKey = type + '_idx', - indexLenKey = type + '_len'; + if (L.Util.invokeEach(types, this.removeEventListener, this, fn, context)) { return this; } - if (!events) { return; } + var events = this[eventsKey], + contextId = context && context !== this && L.stamp(context), + i, len, type, listeners, j, indexKey, indexLenKey, typeIndex, removed; - if (!fn) { - // clear all listeners for a type if function isn't specified - delete events[type]; - delete events[indexKey]; - delete events[indexLenKey]; - return; - } + types = L.Util.splitWords(types); - var contextId = context && context !== this && L.stamp(context), - listeners, i, len, listener, id; + for (i = 0, len = types.length; i < len; i++) { + type = types[i]; + indexKey = type + '_idx'; + indexLenKey = indexKey + '_len'; - if (contextId) { - id = L.stamp(fn) + '_' + contextId; - listeners = events[indexKey]; + typeIndex = events[indexKey]; - if (listeners && listeners[id]) { - listener = listeners[id]; - delete listeners[id]; - events[indexLenKey]--; - } + if (!fn) { + // clear all listeners for a type if function isn't specified + delete events[type]; + delete events[indexKey]; + delete events[indexLenKey]; - } else { - listeners = events[type]; + } else { + listeners = contextId && typeIndex ? typeIndex[contextId] : events[type]; + + if (listeners) { + for (j = listeners.length - 1; j >= 0; j--) { + if ((listeners[j].action === fn) && (!context || (listeners[j].context === context))) { + removed = listeners.splice(j, 1); + // set the old action to a no-op, because it is possible + // that the listener is being iterated over as part of a dispatch + removed[0].action = L.Util.falseFn; + } + } - if (listeners) { - for (i = 0, len = listeners.length; i < len; i++) { - if (listeners[i].fn === fn) { - listener = listeners[i]; - listeners.splice(i, 1); - break; + if (context && typeIndex && (listeners.length === 0)) { + delete typeIndex[contextId]; + events[indexLenKey]--; } } } } - // set the removed listener to noop so that's not called if remove happens in fire - if (listener) { - listener.fn = L.Util.falseFn; - } + return this; }, - fire: function (type, data, propagate) { - if (!this.listens(type, propagate)) { return this; } + clearAllEventListeners: function () { + delete this[eventsKey]; + return this; + }, - var event = L.Util.extend({}, data, {type: type, target: this}), - events = this._events; + fireEvent: function (type, data) { // (String[, Object]) + if (!this.hasEventListeners(type)) { + return this; + } - if (events) { - var typeIndex = events[type + '_idx'], - i, len, listeners, id; + var event = L.Util.extend({}, data, { type: type, target: this }); - if (events[type]) { - // make sure adding/removing listeners inside other listeners won't cause infinite loop - listeners = events[type].slice(); + var events = this[eventsKey], + listeners, i, len, typeIndex, contextId; - for (i = 0, len = listeners.length; i < len; i++) { - listeners[i].fn.call(this, event); - } - } + if (events[type]) { + // make sure adding/removing listeners inside other listeners won't cause infinite loop + listeners = events[type].slice(); - // fire event for the context-indexed listeners as well - for (id in typeIndex) { - typeIndex[id].fn.call(typeIndex[id].ctx, event); + for (i = 0, len = listeners.length; i < len; i++) { + listeners[i].action.call(listeners[i].context, event); } } - if (propagate) { - // propagate the event to parents (set with addEventParent) - this._propagateEvent(event); - } - - return this; - }, + // fire event for the context-indexed listeners as well + typeIndex = events[type + '_idx']; - listens: function (type, propagate) { - var events = this._events; + for (contextId in typeIndex) { + listeners = typeIndex[contextId].slice(); - if (events && (events[type] || events[type + '_len'])) { return true; } - - if (propagate) { - // also check parents for listeners if event propagates - for (var id in this._eventParents) { - if (this._eventParents[id].listens(type, propagate)) { return true; } + if (listeners) { + for (i = 0, len = listeners.length; i < len; i++) { + listeners[i].action.call(listeners[i].context, event); + } } } - return false; + + return this; }, - once: function (types, fn, context) { + addOneTimeEventListener: function (types, fn, context) { - if (typeof types === 'object') { - for (var type in types) { - this.once(type, types[type], fn); - } - return this; - } + if (L.Util.invokeEach(types, this.addOneTimeEventListener, this, fn, context)) { return this; } var handler = L.bind(function () { this - .off(types, fn, context) - .off(types, handler, context); + .removeEventListener(types, fn, context) + .removeEventListener(types, handler, context); }, this); - // add a listener that's executed once and removed after that return this - .on(types, fn, context) - .on(types, handler, context); - }, - - // adds a parent to propagate events to (when you fire with true as a 3rd argument) - addEventParent: function (obj) { - this._eventParents = this._eventParents || {}; - this._eventParents[L.stamp(obj)] = obj; - return this; - }, - - removeEventParent: function (obj) { - if (this._eventParents) { - delete this._eventParents[L.stamp(obj)]; - } - return this; - }, - - _propagateEvent: function (e) { - for (var id in this._eventParents) { - this._eventParents[id].fire(e.type, L.extend({layer: e.target}, e), true); - } + .addEventListener(types, fn, context) + .addEventListener(types, handler, context); } -}); - -var proto = L.Evented.prototype; - -// aliases; we should ditch those eventually -proto.addEventListener = proto.on; -proto.removeEventListener = proto.clearAllEventListeners = proto.off; -proto.addOneTimeEventListener = proto.once; -proto.fireEvent = proto.fire; -proto.hasEventListeners = proto.listens; +}; -L.Mixin = {Events: proto}; +L.Mixin.Events.on = L.Mixin.Events.addEventListener; +L.Mixin.Events.off = L.Mixin.Events.removeEventListener; +L.Mixin.Events.once = L.Mixin.Events.addOneTimeEventListener; +L.Mixin.Events.fire = L.Mixin.Events.fireEvent; /* @@ -568,62 +506,94 @@ L.Mixin = {Events: proto}; (function () { - var ua = navigator.userAgent.toLowerCase(), - doc = document.documentElement, - - ie = 'ActiveXObject' in window, + var ie = 'ActiveXObject' in window, + ielt9 = ie && !document.addEventListener, - webkit = ua.indexOf('webkit') !== -1, + // terrible browser detection to work around Safari / iOS / Android browser bugs + ua = navigator.userAgent.toLowerCase(), + webkit = ua.indexOf('webkit') !== -1, + chrome = ua.indexOf('chrome') !== -1, phantomjs = ua.indexOf('phantom') !== -1, + android = ua.indexOf('android') !== -1, android23 = ua.search('android [23]') !== -1, - chrome = ua.indexOf('chrome') !== -1, + gecko = ua.indexOf('gecko') !== -1, - mobile = typeof orientation !== 'undefined', - msPointer = navigator.msPointerEnabled && navigator.msMaxTouchPoints && !window.PointerEvent, - pointer = (window.PointerEvent && navigator.pointerEnabled && navigator.maxTouchPoints) || msPointer, + mobile = typeof orientation !== undefined + '', + msPointer = window.navigator && window.navigator.msPointerEnabled && + window.navigator.msMaxTouchPoints && !window.PointerEvent, + pointer = (window.PointerEvent && window.navigator.pointerEnabled && window.navigator.maxTouchPoints) || + msPointer, + retina = ('devicePixelRatio' in window && window.devicePixelRatio > 1) || + ('matchMedia' in window && window.matchMedia('(min-resolution:144dpi)') && + window.matchMedia('(min-resolution:144dpi)').matches), + doc = document.documentElement, ie3d = ie && ('transition' in doc.style), webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23, gecko3d = 'MozPerspective' in doc.style, - opera3d = 'OTransition' in doc.style; + opera3d = 'OTransition' in doc.style, + any3d = !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d || opera3d) && !phantomjs; - var retina = 'devicePixelRatio' in window && window.devicePixelRatio > 1; + // PhantomJS has 'ontouchstart' in document.documentElement, but doesn't actually support touch. + // https://github.com/Leaflet/Leaflet/pull/1434#issuecomment-13843151 - if (!retina && 'matchMedia' in window) { - var matches = window.matchMedia('(min-resolution:144dpi)'); - retina = matches && matches.matches; - } + var touch = !window.L_NO_TOUCH && !phantomjs && (function () { + + var startName = 'ontouchstart'; + + // IE10+ (We simulate these into touch* events in L.DomEvent and L.DomEvent.Pointer) or WebKit, etc. + if (pointer || (startName in doc)) { + return true; + } + + // Firefox/Gecko + var div = document.createElement('div'), + supported = false; + + if (!div.setAttribute) { + return false; + } + div.setAttribute(startName, 'return;'); + + if (typeof div[startName] === 'function') { + supported = true; + } + + div.removeAttribute(startName); + div = null; + + return supported; + }()); - var touch = !window.L_NO_TOUCH && !phantomjs && (pointer || 'ontouchstart' in window || - (window.DocumentTouch && document instanceof window.DocumentTouch)); L.Browser = { ie: ie, - ielt9: ie && !document.addEventListener, + ielt9: ielt9, webkit: webkit, - gecko: (ua.indexOf('gecko') !== -1) && !webkit && !window.opera && !ie, - android: ua.indexOf('android') !== -1, + gecko: gecko && !webkit && !window.opera && !ie, + + android: android, android23: android23, + chrome: chrome, - safari: !chrome && ua.indexOf('safari') !== -1, ie3d: ie3d, webkit3d: webkit3d, gecko3d: gecko3d, opera3d: opera3d, - any3d: !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d || opera3d) && !phantomjs, + any3d: any3d, mobile: mobile, mobileWebkit: mobile && webkit, mobileWebkit3d: mobile && webkit3d, mobileOpera: mobile && window.opera, - touch: !!touch, - msPointer: !!msPointer, - pointer: !!pointer, + touch: touch, + msPointer: msPointer, + pointer: pointer, - retina: !!retina + retina: retina }; }()); @@ -706,16 +676,6 @@ L.Point.prototype = { return this; }, - ceil: function () { - return this.clone()._ceil(); - }, - - _ceil: function () { - this.x = Math.ceil(this.x); - this.y = Math.ceil(this.y); - return this; - }, - distanceTo: function (point) { point = L.point(point); @@ -896,12 +856,16 @@ L.Transformation.prototype = { L.DomUtil = { get: function (id) { - return typeof id === 'string' ? document.getElementById(id) : id; + return (typeof id === 'string' ? document.getElementById(id) : id); }, getStyle: function (el, style) { - var value = el.style[style] || (el.currentStyle && el.currentStyle[style]); + var value = el.style[style]; + + if (!value && el.currentStyle) { + value = el.currentStyle[style]; + } if ((!value || value === 'auto') && document.defaultView) { var css = document.defaultView.getComputedStyle(el, null); @@ -911,45 +875,91 @@ L.DomUtil = { return value === 'auto' ? null : value; }, - create: function (tagName, className, container) { + getViewportOffset: function (element) { - var el = document.createElement(tagName); - el.className = className; + var top = 0, + left = 0, + el = element, + docBody = document.body, + docEl = document.documentElement, + pos; - if (container) { - container.appendChild(el); - } + do { + top += el.offsetTop || 0; + left += el.offsetLeft || 0; - return el; - }, + //add borders + top += parseInt(L.DomUtil.getStyle(el, 'borderTopWidth'), 10) || 0; + left += parseInt(L.DomUtil.getStyle(el, 'borderLeftWidth'), 10) || 0; - remove: function (el) { - var parent = el.parentNode; - if (parent) { - parent.removeChild(el); - } + pos = L.DomUtil.getStyle(el, 'position'); + + if (el.offsetParent === docBody && pos === 'absolute') { break; } + + if (pos === 'fixed') { + top += docBody.scrollTop || docEl.scrollTop || 0; + left += docBody.scrollLeft || docEl.scrollLeft || 0; + break; + } + + if (pos === 'relative' && !el.offsetLeft) { + var width = L.DomUtil.getStyle(el, 'width'), + maxWidth = L.DomUtil.getStyle(el, 'max-width'), + r = el.getBoundingClientRect(); + + if (width !== 'none' || maxWidth !== 'none') { + left += r.left + el.clientLeft; + } + + //calculate full y offset since we're breaking out of the loop + top += r.top + (docBody.scrollTop || docEl.scrollTop || 0); + + break; + } + + el = el.offsetParent; + + } while (el); + + el = element; + + do { + if (el === docBody) { break; } + + top -= el.scrollTop || 0; + left -= el.scrollLeft || 0; + + el = el.parentNode; + } while (el); + + return new L.Point(left, top); }, - empty: function (el) { - while (el.firstChild) { - el.removeChild(el.firstChild); + documentIsLtr: function () { + if (!L.DomUtil._docIsLtrCached) { + L.DomUtil._docIsLtrCached = true; + L.DomUtil._docIsLtr = L.DomUtil.getStyle(document.body, 'direction') === 'ltr'; } + return L.DomUtil._docIsLtr; }, - toFront: function (el) { - el.parentNode.appendChild(el); - }, + create: function (tagName, className, container) { + + var el = document.createElement(tagName); + el.className = className; + + if (container) { + container.appendChild(el); + } - toBack: function (el) { - var parent = el.parentNode; - parent.insertBefore(el, parent.firstChild); + return el; }, hasClass: function (el, name) { if (el.classList !== undefined) { return el.classList.contains(name); } - var className = L.DomUtil.getClass(el); + var className = L.DomUtil._getClass(el); return className.length > 0 && new RegExp('(^|\\s)' + name + '(\\s|$)').test(className); }, @@ -960,8 +970,8 @@ L.DomUtil = { el.classList.add(classes[i]); } } else if (!L.DomUtil.hasClass(el, name)) { - var className = L.DomUtil.getClass(el); - L.DomUtil.setClass(el, (className ? className + ' ' : '') + name); + var className = L.DomUtil._getClass(el); + L.DomUtil._setClass(el, (className ? className + ' ' : '') + name); } }, @@ -969,11 +979,11 @@ L.DomUtil = { if (el.classList !== undefined) { el.classList.remove(name); } else { - L.DomUtil.setClass(el, L.Util.trim((' ' + L.DomUtil.getClass(el) + ' ').replace(' ' + name + ' ', ' '))); + L.DomUtil._setClass(el, L.Util.trim((' ' + L.DomUtil._getClass(el) + ' ').replace(' ' + name + ' ', ' '))); } }, - setClass: function (el, name) { + _setClass: function (el, name) { if (el.className.baseVal === undefined) { el.className = name; } else { @@ -982,7 +992,7 @@ L.DomUtil = { } }, - getClass: function (el) { + _getClass: function (el) { return el.className.baseVal === undefined ? el.className : el.className.baseVal; }, @@ -1028,20 +1038,33 @@ L.DomUtil = { return false; }, - setTransform: function (el, offset, scale) { - var pos = offset || new L.Point(0, 0); + getTranslateString: function (point) { + // on WebKit browsers (Chrome/Safari/iOS Safari/Android) using translate3d instead of translate + // makes animation smoother as it ensures HW accel is used. Firefox 13 doesn't care + // (same speed either way), Opera 12 doesn't support translate3d + + var is3d = L.Browser.webkit3d, + open = 'translate' + (is3d ? '3d' : '') + '(', + close = (is3d ? ',0' : '') + ')'; + + return open + point.x + 'px,' + point.y + 'px' + close; + }, + + getScaleString: function (scale, origin) { - el.style[L.DomUtil.TRANSFORM] = - 'translate3d(' + pos.x + 'px,' + pos.y + 'px' + ',0)' + (scale ? ' scale(' + scale + ')' : ''); + var preTranslateStr = L.DomUtil.getTranslateString(origin.add(origin.multiplyBy(-1 * scale))), + scaleStr = ' scale(' + scale + ') '; + + return preTranslateStr + scaleStr; }, - setPosition: function (el, point, no3d) { // (HTMLElement, Point[, Boolean]) + setPosition: function (el, point, disable3D) { // (HTMLElement, Point[, Boolean]) // jshint camelcase: false el._leaflet_pos = point; - if (L.Browser.any3d && !no3d) { - L.DomUtil.setTransform(el, point); + if (!disable3D && L.Browser.any3d) { + el.style[L.DomUtil.TRANSFORM] = L.DomUtil.getTranslateString(point); } else { el.style.left = point.x + 'px'; el.style.top = point.y + 'px'; @@ -1058,56 +1081,63 @@ L.DomUtil = { }; -(function () { - // prefix style property names - - L.DomUtil.TRANSFORM = L.DomUtil.testProp( - ['transform', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform']); - +// prefix style property names - // webkitTransition comes first because some browser versions that drop vendor prefix don't do - // the same for the transitionend event, in particular the Android 4.1 stock browser +L.DomUtil.TRANSFORM = L.DomUtil.testProp( + ['transform', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform']); - var transition = L.DomUtil.TRANSITION = L.DomUtil.testProp( - ['webkitTransition', 'transition', 'OTransition', 'MozTransition', 'msTransition']); +// webkitTransition comes first because some browser versions that drop vendor prefix don't do +// the same for the transitionend event, in particular the Android 4.1 stock browser - L.DomUtil.TRANSITION_END = - transition === 'webkitTransition' || transition === 'OTransition' ? transition + 'End' : 'transitionend'; - - - if ('onselectstart' in document) { - L.DomUtil.disableTextSelection = function () { - L.DomEvent.on(window, 'selectstart', L.DomEvent.preventDefault); - }; - L.DomUtil.enableTextSelection = function () { - L.DomEvent.off(window, 'selectstart', L.DomEvent.preventDefault); - }; +L.DomUtil.TRANSITION = L.DomUtil.testProp( + ['webkitTransition', 'transition', 'OTransition', 'MozTransition', 'msTransition']); - } else { - var userSelectProperty = L.DomUtil.testProp( - ['userSelect', 'WebkitUserSelect', 'OUserSelect', 'MozUserSelect', 'msUserSelect']); +L.DomUtil.TRANSITION_END = + L.DomUtil.TRANSITION === 'webkitTransition' || L.DomUtil.TRANSITION === 'OTransition' ? + L.DomUtil.TRANSITION + 'End' : 'transitionend'; - L.DomUtil.disableTextSelection = function () { - if (userSelectProperty) { - var style = document.documentElement.style; - this._userSelect = style[userSelectProperty]; - style[userSelectProperty] = 'none'; - } - }; - L.DomUtil.enableTextSelection = function () { - if (userSelectProperty) { - document.documentElement.style[userSelectProperty] = this._userSelect; - delete this._userSelect; - } - }; - } +(function () { + if ('onselectstart' in document) { + L.extend(L.DomUtil, { + disableTextSelection: function () { + L.DomEvent.on(window, 'selectstart', L.DomEvent.preventDefault); + }, + + enableTextSelection: function () { + L.DomEvent.off(window, 'selectstart', L.DomEvent.preventDefault); + } + }); + } else { + var userSelectProperty = L.DomUtil.testProp( + ['userSelect', 'WebkitUserSelect', 'OUserSelect', 'MozUserSelect', 'msUserSelect']); + + L.extend(L.DomUtil, { + disableTextSelection: function () { + if (userSelectProperty) { + var style = document.documentElement.style; + this._userSelect = style[userSelectProperty]; + style[userSelectProperty] = 'none'; + } + }, + + enableTextSelection: function () { + if (userSelectProperty) { + document.documentElement.style[userSelectProperty] = this._userSelect; + delete this._userSelect; + } + } + }); + } + + L.extend(L.DomUtil, { + disableImageDrag: function () { + L.DomEvent.on(window, 'dragstart', L.DomEvent.preventDefault); + }, - L.DomUtil.disableImageDrag = function () { - L.DomEvent.on(window, 'dragstart', L.DomEvent.preventDefault); - }; - L.DomUtil.enableImageDrag = function () { - L.DomEvent.off(window, 'dragstart', L.DomEvent.preventDefault); - }; + enableImageDrag: function () { + L.DomEvent.off(window, 'dragstart', L.DomEvent.preventDefault); + } + }); })(); @@ -1115,21 +1145,30 @@ L.DomUtil = { * L.LatLng represents a geographical point with latitude and longitude coordinates. */ -L.LatLng = function (lat, lng, alt) { +L.LatLng = function (lat, lng, alt) { // (Number, Number, Number) + lat = parseFloat(lat); + lng = parseFloat(lng); + if (isNaN(lat) || isNaN(lng)) { throw new Error('Invalid LatLng object: (' + lat + ', ' + lng + ')'); } - this.lat = +lat; - this.lng = +lng; + this.lat = lat; + this.lng = lng; if (alt !== undefined) { - this.alt = +alt; + this.alt = parseFloat(alt); } }; +L.extend(L.LatLng, { + DEG_TO_RAD: Math.PI / 180, + RAD_TO_DEG: 180 / Math.PI, + MAX_MARGIN: 1.0E-9 // max margin of error for the "equals" check +}); + L.LatLng.prototype = { - equals: function (obj, maxMargin) { + equals: function (obj) { // (LatLng) -> Boolean if (!obj) { return false; } obj = L.latLng(obj); @@ -1138,37 +1177,56 @@ L.LatLng.prototype = { Math.abs(this.lat - obj.lat), Math.abs(this.lng - obj.lng)); - return margin <= (maxMargin === undefined ? 1.0E-9 : maxMargin); + return margin <= L.LatLng.MAX_MARGIN; }, - toString: function (precision) { + toString: function (precision) { // (Number) -> String return 'LatLng(' + L.Util.formatNum(this.lat, precision) + ', ' + L.Util.formatNum(this.lng, precision) + ')'; }, - distanceTo: function (other) { - return L.CRS.Earth.distance(this, L.latLng(other)); + // Haversine distance formula, see http://en.wikipedia.org/wiki/Haversine_formula + // TODO move to projection code, LatLng shouldn't know about Earth + distanceTo: function (other) { // (LatLng) -> Number + other = L.latLng(other); + + var R = 6378137, // earth radius in meters + d2r = L.LatLng.DEG_TO_RAD, + dLat = (other.lat - this.lat) * d2r, + dLon = (other.lng - this.lng) * d2r, + lat1 = this.lat * d2r, + lat2 = other.lat * d2r, + sin1 = Math.sin(dLat / 2), + sin2 = Math.sin(dLon / 2); + + var a = sin1 * sin1 + sin2 * sin2 * Math.cos(lat1) * Math.cos(lat2); + + return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); }, - wrap: function () { - return L.CRS.Earth.wrapLatLng(this); - } -}; + wrap: function (a, b) { // (Number, Number) -> LatLng + var lng = this.lng; + + a = a || -180; + b = b || 180; + lng = (lng + b) % (b - a) + (lng < a || lng === b ? b : a); -// constructs LatLng with different signatures -// (LatLng) or ([Number, Number]) or (Number, Number) or (Object) + return new L.LatLng(this.lat, lng); + } +}; -L.latLng = function (a, b) { +L.latLng = function (a, b) { // (LatLng) or ([Number, Number]) or (Number, Number) if (a instanceof L.LatLng) { return a; } - if (L.Util.isArray(a) && typeof a[0] !== 'object') { - if (a.length === 3) { + if (L.Util.isArray(a)) { + if (typeof a[0] === 'number' || typeof a[0] === 'string') { return new L.LatLng(a[0], a[1], a[2]); + } else { + return null; } - return new L.LatLng(a[0], a[1]); } if (a === undefined || a === null) { return a; @@ -1199,37 +1257,32 @@ L.LatLngBounds = function (southWest, northEast) { // (LatLng, LatLng) or (LatLn }; L.LatLngBounds.prototype = { - // extend the bounds to contain the given point or bounds extend: function (obj) { // (LatLng) or (LatLngBounds) - var sw = this._southWest, - ne = this._northEast, - sw2, ne2; - - if (obj instanceof L.LatLng) { - sw2 = obj; - ne2 = obj; - - } else if (obj instanceof L.LatLngBounds) { - sw2 = obj._southWest; - ne2 = obj._northEast; - - if (!sw2 || !ne2) { return this; } + if (!obj) { return this; } + var latLng = L.latLng(obj); + if (latLng !== null) { + obj = latLng; } else { - return obj ? this.extend(L.latLng(obj) || L.latLngBounds(obj)) : this; + obj = L.latLngBounds(obj); } - if (!sw && !ne) { - this._southWest = new L.LatLng(sw2.lat, sw2.lng); - this._northEast = new L.LatLng(ne2.lat, ne2.lng); - } else { - sw.lat = Math.min(sw2.lat, sw.lat); - sw.lng = Math.min(sw2.lng, sw.lng); - ne.lat = Math.max(ne2.lat, ne.lat); - ne.lng = Math.max(ne2.lng, ne.lng); - } + if (obj instanceof L.LatLng) { + if (!this._southWest && !this._northEast) { + this._southWest = new L.LatLng(obj.lat, obj.lng); + this._northEast = new L.LatLng(obj.lat, obj.lng); + } else { + this._southWest.lat = Math.min(obj.lat, this._southWest.lat); + this._southWest.lng = Math.min(obj.lng, this._southWest.lng); + this._northEast.lat = Math.max(obj.lat, this._northEast.lat); + this._northEast.lng = Math.max(obj.lng, this._northEast.lng); + } + } else if (obj instanceof L.LatLngBounds) { + this.extend(obj._southWest); + this.extend(obj._northEast); + } return this; }, @@ -1348,22 +1401,10 @@ L.latLngBounds = function (a, b) { // (LatLngBounds) or (LatLng, LatLng) /* - * Simple equirectangular (Plate Carree) projection, used by CRS like EPSG:4326 and Simple. + * L.Projection contains various geographical projections used by CRS classes. */ L.Projection = {}; - -L.Projection.LonLat = { - project: function (latlng) { - return new L.Point(latlng.lng, latlng.lat); - }, - - unproject: function (point) { - return new L.LatLng(point.y, point.x); - }, - - bounds: L.bounds([-180, -90], [180, 90]) -}; /* @@ -1371,95 +1412,75 @@ L.Projection.LonLat = { */ L.Projection.SphericalMercator = { + MAX_LATITUDE: 85.0511287798, - R: 6378137, + project: function (latlng) { // (LatLng) -> Point + var d = L.LatLng.DEG_TO_RAD, + max = this.MAX_LATITUDE, + lat = Math.max(Math.min(max, latlng.lat), -max), + x = latlng.lng * d, + y = lat * d; - project: function (latlng) { - var d = Math.PI / 180, - max = 1 - 1E-15, - sin = Math.max(Math.min(Math.sin(latlng.lat * d), max), -max); + y = Math.log(Math.tan((Math.PI / 4) + (y / 2))); - return new L.Point( - this.R * latlng.lng * d, - this.R * Math.log((1 + sin) / (1 - sin)) / 2); + return new L.Point(x, y); }, - unproject: function (point) { - var d = 180 / Math.PI; + unproject: function (point) { // (Point, Boolean) -> LatLng + var d = L.LatLng.RAD_TO_DEG, + lng = point.x * d, + lat = (2 * Math.atan(Math.exp(point.y)) - (Math.PI / 2)) * d; - return new L.LatLng( - (2 * Math.atan(Math.exp(point.y / this.R)) - (Math.PI / 2)) * d, - point.x * d / this.R); - }, + return new L.LatLng(lat, lng); + } +}; + + +/* + * Simple equirectangular (Plate Carree) projection, used by CRS like EPSG:4326 and Simple. + */ + +L.Projection.LonLat = { + project: function (latlng) { + return new L.Point(latlng.lng, latlng.lat); + }, - bounds: (function () { - var d = 6378137 * Math.PI; - return L.bounds([-d, -d], [d, d]); - })() + unproject: function (point) { + return new L.LatLng(point.y, point.x); + } }; /* - * L.CRS is the base object for all defined CRS (Coordinate Reference Systems) in Leaflet. + * L.CRS is a base object for all defined CRS (Coordinate Reference Systems) in Leaflet. */ L.CRS = { - // converts geo coords to pixel ones - latLngToPoint: function (latlng, zoom) { + latLngToPoint: function (latlng, zoom) { // (LatLng, Number) -> Point var projectedPoint = this.projection.project(latlng), scale = this.scale(zoom); return this.transformation._transform(projectedPoint, scale); }, - // converts pixel coords to geo coords - pointToLatLng: function (point, zoom) { + pointToLatLng: function (point, zoom) { // (Point, Number[, Boolean]) -> LatLng var scale = this.scale(zoom), untransformedPoint = this.transformation.untransform(point, scale); return this.projection.unproject(untransformedPoint); }, - // converts geo coords to projection-specific coords (e.g. in meters) project: function (latlng) { return this.projection.project(latlng); }, - // converts projected coords to geo coords - unproject: function (point) { - return this.projection.unproject(point); - }, - - // defines how the world scales with zoom scale: function (zoom) { return 256 * Math.pow(2, zoom); }, - // returns the bounds of the world in projected coords if applicable - getProjectedBounds: function (zoom) { - if (this.infinite) { return null; } - - var b = this.projection.bounds, - s = this.scale(zoom), - min = this.transformation.transform(b.min, s), - max = this.transformation.transform(b.max, s); - - return L.bounds(min, max); - }, - - // whether a coordinate axis wraps in a given range (e.g. longitude from -180 to 180); depends on CRS - // wrapLng: [min, max], - // wrapLat: [min, max], - - // if true, the coordinate space will be unbounded (infinite in all directions) - // infinite: false, - - // wraps geo coords in certain ranges if applicable - wrapLatLng: function (latlng) { - var lng = this.wrapLng ? L.Util.wrapNum(latlng.lng, this.wrapLng, true) : latlng.lng, - lat = this.wrapLat ? L.Util.wrapNum(latlng.lat, this.wrapLat, true) : latlng.lat; - - return L.latLng(lat, lng); + getSize: function (zoom) { + var s = this.scale(zoom); + return L.point(s, s); } }; @@ -1474,53 +1495,26 @@ L.CRS.Simple = L.extend({}, L.CRS, { scale: function (zoom) { return Math.pow(2, zoom); - }, - - distance: function (latlng1, latlng2) { - var dx = latlng2.lng - latlng1.lng, - dy = latlng2.lat - latlng1.lat; - - return Math.sqrt(dx * dx + dy * dy); - }, - - infinite: true -}); - - -/* - * L.CRS.Earth is the base class for all CRS representing Earth. - */ - -L.CRS.Earth = L.extend({}, L.CRS, { - wrapLng: [-180, 180], - - R: 6378137, - - // distane between two geographical points using spherical law of cosines approximation - distance: function (latlng1, latlng2) { - var rad = Math.PI / 180, - lat1 = latlng1.lat * rad, - lat2 = latlng2.lat * rad, - a = Math.sin(lat1) * Math.sin(lat2) + - Math.cos(lat1) * Math.cos(lat2) * Math.cos((latlng2.lng - latlng1.lng) * rad); - - return this.R * Math.acos(Math.min(a, 1)); } }); /* - * L.CRS.EPSG3857 (Spherical Mercator) is the most common CRS for web mapping and is used by Leaflet by default. + * L.CRS.EPSG3857 (Spherical Mercator) is the most common CRS for web mapping + * and is used by Leaflet by default. */ -L.CRS.EPSG3857 = L.extend({}, L.CRS.Earth, { +L.CRS.EPSG3857 = L.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), - transformation: (function () { - var scale = 0.5 / (Math.PI * L.Projection.SphericalMercator.R); - return new L.Transformation(scale, 0.5, -scale, 0.5); - }()) + project: function (latlng) { // (LatLng) -> Point + var projectedPoint = this.projection.project(latlng), + earthRadius = 6378137; + return projectedPoint.multiplyBy(earthRadius); + } }); L.CRS.EPSG900913 = L.extend({}, L.CRS.EPSG3857, { @@ -1532,10 +1526,11 @@ L.CRS.EPSG900913 = L.extend({}, L.CRS.EPSG3857, { * L.CRS.EPSG4326 is a CRS popular among advanced GIS specialists. */ -L.CRS.EPSG4326 = L.extend({}, L.CRS.Earth, { +L.CRS.EPSG4326 = L.extend({}, L.CRS, { code: 'EPSG:4326', + projection: L.Projection.LonLat, - transformation: new L.Transformation(1 / 180, 1, -1 / 180, 0.5) + transformation: new L.Transformation(1 / 360, 0.5, -1 / 360, 0.5) }); @@ -1543,7 +1538,9 @@ L.CRS.EPSG4326 = L.extend({}, L.CRS.Earth, { * L.Map is the central class of the API - it is used to create a map. */ -L.Map = L.Evented.extend({ +L.Map = L.Class.extend({ + + includes: L.Mixin.Events, options: { crs: L.CRS.EPSG3857, @@ -1554,14 +1551,15 @@ L.Map = L.Evented.extend({ layers: Array, */ - fadeAnimation: true, + fadeAnimation: L.DomUtil.TRANSITION && !L.Browser.android23, trackResize: true, - markerZoomAnimation: true + markerZoomAnimation: L.DomUtil.TRANSITION && L.Browser.any3d }, initialize: function (id, options) { // (HTMLElement or String, Object) options = L.setOptions(this, options); + this._initContainer(id); this._initLayout(); @@ -1574,21 +1572,19 @@ L.Map = L.Evented.extend({ this.setMaxBounds(options.maxBounds); } - if (options.zoom !== undefined) { - this._zoom = this._limitZoom(options.zoom); - } - if (options.center && options.zoom !== undefined) { this.setView(L.latLng(options.center), options.zoom, {reset: true}); } this._handlers = []; + this._layers = {}; this._zoomBoundLayers = {}; + this._tileLayersNum = 0; this.callInitHooks(); - this._addLayers(this.options.layers); + this._addLayers(options.layers); }, @@ -1636,16 +1632,15 @@ L.Map = L.Evented.extend({ var paddingTL = L.point(options.paddingTopLeft || options.padding || [0, 0]), paddingBR = L.point(options.paddingBottomRight || options.padding || [0, 0]), - zoom = this.getBoundsZoom(bounds, false, paddingTL.add(paddingBR)); - - zoom = options.maxZoom ? Math.min(options.maxZoom, zoom) : zoom; - - var paddingOffset = paddingBR.subtract(paddingTL).divideBy(2), + zoom = this.getBoundsZoom(bounds, false, paddingTL.add(paddingBR)), + paddingOffset = paddingBR.subtract(paddingTL).divideBy(2), swPoint = this.project(bounds.getSouthWest(), zoom), nePoint = this.project(bounds.getNorthEast(), zoom), center = this.unproject(swPoint.add(nePoint).divideBy(2).add(paddingOffset), zoom); + zoom = options && options.maxZoom ? Math.min(options.maxZoom, zoom) : zoom; + return this.setView(center, zoom, options); }, @@ -1673,14 +1668,14 @@ L.Map = L.Evented.extend({ this.options.maxBounds = bounds; if (!bounds) { - return this.off('moveend', this._panInsideMaxBounds); + return this.off('moveend', this._panInsideMaxBounds, this); } if (this._loaded) { this._panInsideMaxBounds(); } - return this.on('moveend', this._panInsideMaxBounds); + return this.on('moveend', this._panInsideMaxBounds, this); }, panInsideBounds: function (bounds, options) { @@ -1692,6 +1687,78 @@ L.Map = L.Evented.extend({ return this.panTo(newCenter, options); }, + addLayer: function (layer) { + // TODO method is too big, refactor + + var id = L.stamp(layer); + + if (this._layers[id]) { return this; } + + this._layers[id] = layer; + + // TODO getMaxZoom, getMinZoom in ILayer (instead of options) + if (layer.options && (!isNaN(layer.options.maxZoom) || !isNaN(layer.options.minZoom))) { + this._zoomBoundLayers[id] = layer; + this._updateZoomLevels(); + } + + // TODO looks ugly, refactor!!! + if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) { + this._tileLayersNum++; + this._tileLayersToLoad++; + layer.on('load', this._onTileLayerLoad, this); + } + + if (this._loaded) { + this._layerAdd(layer); + } + + return this; + }, + + removeLayer: function (layer) { + var id = L.stamp(layer); + + if (!this._layers[id]) { return this; } + + if (this._loaded) { + layer.onRemove(this); + } + + delete this._layers[id]; + + if (this._loaded) { + this.fire('layerremove', {layer: layer}); + } + + if (this._zoomBoundLayers[id]) { + delete this._zoomBoundLayers[id]; + this._updateZoomLevels(); + } + + // TODO looks ugly, refactor + if (this.options.zoomAnimation && L.TileLayer && (layer instanceof L.TileLayer)) { + this._tileLayersNum--; + this._tileLayersToLoad--; + layer.off('load', this._onTileLayerLoad, this); + } + + return this; + }, + + hasLayer: function (layer) { + if (!layer) { return false; } + + return (L.stamp(layer) in this._layers); + }, + + eachLayer: function (method, context) { + for (var i in this._layers) { + method.call(context, this._layers[i]); + } + return this; + }, + invalidateSize: function (options) { if (!this._loaded) { return this; } @@ -1751,6 +1818,9 @@ L.Map = L.Evented.extend({ }, remove: function () { + if (this._loaded) { + this.fire('unload'); + } this._initEvents('off'); @@ -1761,31 +1831,16 @@ L.Map = L.Evented.extend({ this._container._leaflet = undefined; } - L.DomUtil.remove(this._mapPane); - + this._clearPanes(); if (this._clearControlPos) { this._clearControlPos(); } this._clearHandlers(); - if (this._loaded) { - this.fire('unload'); - } - return this; }, - createPane: function (name, container) { - var className = 'leaflet-pane' + (name ? ' leaflet-' + name.replace('Pane', '') + '-pane' : ''), - pane = L.DomUtil.create('div', className, container || this._mapPane); - - if (name) { - this._panes[name] = pane; - } - return pane; - }, - // public methods for getting map state @@ -1811,7 +1866,9 @@ L.Map = L.Evented.extend({ }, getMinZoom: function () { - return this.options.minZoom === undefined ? this._layersMinZoom || 0 : this.options.minZoom; + return this.options.minZoom === undefined ? + (this._layersMinZoom === undefined ? 0 : this._layersMinZoom) : + this.options.minZoom; }, getMaxZoom: function () { @@ -1870,14 +1927,6 @@ L.Map = L.Evented.extend({ return this._initialTopLeftPoint; }, - getPixelWorldBounds: function () { - return this.options.crs.getProjectedBounds(this.getZoom()); - }, - - getPane: function (pane) { - return typeof pane === 'string' ? this._panes[pane] : pane; - }, - getPanes: function () { return this._panes; }, @@ -1921,14 +1970,6 @@ L.Map = L.Evented.extend({ return projectedPoint._subtract(this.getPixelOrigin()); }, - wrapLatLng: function (latlng) { - return this.options.crs.wrapLatLng(L.latLng(latlng)); - }, - - distance: function (latlng1, latlng2) { - return this.options.crs.distance(L.latLng(latlng1), L.latLng(latlng2)); - }, - containerPointToLayerPoint: function (point) { // (Point) return L.point(point).subtract(this._getMapPanePos()); }, @@ -1976,14 +2017,11 @@ L.Map = L.Evented.extend({ _initLayout: function () { var container = this._container; - this._fadeAnimated = this.options.fadeAnimation && L.Browser.any3d; - L.DomUtil.addClass(container, 'leaflet-container' + (L.Browser.touch ? ' leaflet-touch' : '') + (L.Browser.retina ? ' leaflet-retina' : '') + (L.Browser.ielt9 ? ' leaflet-oldie' : '') + - (L.Browser.safari ? ' leaflet-safari' : '') + - (this._fadeAnimated ? ' leaflet-fade-anim' : '')); + (this.options.fadeAnimation ? ' leaflet-fade-anim' : '')); var position = L.DomUtil.getStyle(container, 'position'); @@ -2001,17 +2039,37 @@ L.Map = L.Evented.extend({ _initPanes: function () { var panes = this._panes = {}; - this._mapPane = this.createPane('mapPane', this._container); + this._mapPane = panes.mapPane = this._createPane('leaflet-map-pane', this._container); - this.createPane('tilePane'); - this.createPane('shadowPane'); - this.createPane('overlayPane'); - this.createPane('markerPane'); - this.createPane('popupPane'); + this._tilePane = panes.tilePane = this._createPane('leaflet-tile-pane', this._mapPane); + panes.objectsPane = this._createPane('leaflet-objects-pane', this._mapPane); + panes.shadowPane = this._createPane('leaflet-shadow-pane'); + panes.overlayPane = this._createPane('leaflet-overlay-pane'); + panes.markerPane = this._createPane('leaflet-marker-pane'); + panes.popupPane = this._createPane('leaflet-popup-pane'); + + var zoomHide = ' leaflet-zoom-hide'; if (!this.options.markerZoomAnimation) { - L.DomUtil.addClass(panes.markerPane, 'leaflet-zoom-hide'); - L.DomUtil.addClass(panes.shadowPane, 'leaflet-zoom-hide'); + L.DomUtil.addClass(panes.markerPane, zoomHide); + L.DomUtil.addClass(panes.shadowPane, zoomHide); + L.DomUtil.addClass(panes.popupPane, zoomHide); + } + }, + + _createPane: function (className, container) { + return L.DomUtil.create('div', className, container || this._panes.objectsPane); + }, + + _clearPanes: function () { + this._container.removeChild(this._mapPane); + }, + + _addLayers: function (layers) { + layers = layers ? (L.Util.isArray(layers) ? layers : [layers]) : []; + + for (var i = 0, len = layers.length; i < len; i++) { + this.addLayer(layers[i]); } }, @@ -2041,6 +2099,8 @@ L.Map = L.Evented.extend({ this._initialTopLeftPoint._add(this._getMapPanePos()); } + this._tileLayersToLoad = this._tileLayersNum; + var loading = !this._loaded; this._loaded = true; @@ -2048,6 +2108,7 @@ L.Map = L.Evented.extend({ if (loading) { this.fire('load'); + this.eachLayer(this._layerAdd, this); } this.fire('move'); @@ -2067,6 +2128,34 @@ L.Map = L.Evented.extend({ return this.getMaxZoom() - this.getMinZoom(); }, + _updateZoomLevels: function () { + var i, + minZoom = Infinity, + maxZoom = -Infinity, + oldZoomSpan = this._getZoomSpan(); + + for (i in this._zoomBoundLayers) { + var layer = this._zoomBoundLayers[i]; + if (!isNaN(layer.options.minZoom)) { + minZoom = Math.min(minZoom, layer.options.minZoom); + } + if (!isNaN(layer.options.maxZoom)) { + maxZoom = Math.max(maxZoom, layer.options.maxZoom); + } + } + + if (i === undefined) { // we have no tilelayers + this._layersMaxZoom = this._layersMinZoom = undefined; + } else { + this._layersMaxZoom = maxZoom; + this._layersMinZoom = minZoom; + } + + if (oldZoomSpan !== this._getZoomSpan()) { + this.fire('zoomlevelschange'); + } + }, + _panInsideMaxBounds: function () { this.panInsideBounds(this.options.maxBounds); }, @@ -2084,9 +2173,15 @@ L.Map = L.Evented.extend({ onOff = onOff || 'on'; - L.DomEvent[onOff](this._container, - 'click dblclick mousedown mouseup mouseenter mouseleave mousemove contextmenu', - this._handleMouseEvent, this); + L.DomEvent[onOff](this._container, 'click', this._onMouseClick, this); + + var events = ['dblclick', 'mousedown', 'mouseup', 'mouseenter', + 'mouseleave', 'mousemove', 'contextmenu'], + i, len; + + for (i = 0, len = events.length; i < len; i++) { + L.DomEvent[onOff](this._container, events[i], this._fireMouseEvent, this); + } if (this.options.trackResize) { L.DomEvent[onOff](window, 'resize', this._onResize, this); @@ -2099,43 +2194,46 @@ L.Map = L.Evented.extend({ function () { this.invalidateSize({debounceMoveend: true}); }, this, false, this._container); }, - _handleMouseEvent: function (e) { - if (!this._loaded) { return; } + _onMouseClick: function (e) { + if (!this._loaded || (!e._simulated && + ((this.dragging && this.dragging.moved()) || + (this.boxZoom && this.boxZoom.moved()))) || + L.DomEvent._skipped(e)) { return; } - this._fireMouseEvent(this, e, - e.type === 'mouseenter' ? 'mouseover' : - e.type === 'mouseleave' ? 'mouseout' : e.type); + this.fire('preclick'); + this._fireMouseEvent(e); }, - _fireMouseEvent: function (obj, e, type, propagate, latlng) { - type = type || e.type; + _fireMouseEvent: function (e) { + if (!this._loaded || L.DomEvent._skipped(e)) { return; } - if (L.DomEvent._skipped(e)) { return; } - if (type === 'click') { - var draggableObj = obj.options.draggable === true ? obj : this; - if (!e._simulated && ((draggableObj.dragging && draggableObj.dragging.moved()) || - (this.boxZoom && this.boxZoom.moved()))) { return; } - obj.fire('preclick'); - } + var type = e.type; + + type = (type === 'mouseenter' ? 'mouseover' : (type === 'mouseleave' ? 'mouseout' : type)); - if (!obj.listens(type, propagate)) { return; } + if (!this.hasEventListeners(type)) { return; } if (type === 'contextmenu') { L.DomEvent.preventDefault(e); } - if (type === 'click' || type === 'dblclick' || type === 'contextmenu') { - L.DomEvent.stopPropagation(e); - } - var data = { - originalEvent: e, - containerPoint: this.mouseEventToContainerPoint(e) - }; + var containerPoint = this.mouseEventToContainerPoint(e), + layerPoint = this.containerPointToLayerPoint(containerPoint), + latlng = this.layerPointToLatLng(layerPoint); - data.layerPoint = this.containerPointToLayerPoint(data.containerPoint); - data.latlng = latlng || this.layerPointToLatLng(data.layerPoint); + this.fire(type, { + latlng: latlng, + layerPoint: layerPoint, + containerPoint: containerPoint, + originalEvent: e + }); + }, - obj.fire(type, data, propagate); + _onTileLayerLoad: function () { + this._tileLayersToLoad--; + if (this._tileLayersNum && !this._tileLayersToLoad) { + this.fire('tilelayersload'); + } }, _clearHandlers: function () { @@ -2146,13 +2244,18 @@ L.Map = L.Evented.extend({ whenReady: function (callback, context) { if (this._loaded) { - callback.call(context || this, {target: this}); + callback.call(context || this, this); } else { this.on('load', callback, context); } return this; }, + _layerAdd: function (layer) { + layer.onAdd(this); + this.fire('layeradd', {layer: layer}); + }, + // private methods for getting map state @@ -2243,794 +2346,112 @@ L.map = function (id, options) { }; - -L.Layer = L.Evented.extend({ - - options: { - pane: 'overlayPane' - }, - - addTo: function (map) { - map.addLayer(this); - return this; - }, - - remove: function () { - return this.removeFrom(this._map || this._mapToAdd); - }, - - removeFrom: function (obj) { - if (obj) { - obj.removeLayer(this); - } - return this; - }, - - getPane: function (name) { - return this._map.getPane(name ? (this.options[name] || name) : this.options.pane); - }, - - _layerAdd: function (e) { - var map = e.target; - - // check in case layer gets added and then removed before the map is ready - if (!map.hasLayer(this)) { return; } - - this._map = map; - this._zoomAnimated = map._zoomAnimated; - - this.onAdd(map); - - if (this.getAttribution && this._map.attributionControl) { - this._map.attributionControl.addAttribution(this.getAttribution()); - } - - if (this.getEvents) { - map.on(this.getEvents(), this); - } - - this.fire('add'); - map.fire('layeradd', {layer: this}); - } -}); - - -L.Map.include({ - addLayer: function (layer) { - var id = L.stamp(layer); - if (this._layers[id]) { return layer; } - this._layers[id] = layer; - - layer._mapToAdd = this; - - if (layer.beforeAdd) { - layer.beforeAdd(this); - } - - this.whenReady(layer._layerAdd, layer); - - return this; - }, - - removeLayer: function (layer) { - var id = L.stamp(layer); - - if (!this._layers[id]) { return this; } - - if (this._loaded) { - layer.onRemove(this); - } - - if (layer.getAttribution && this.attributionControl) { - this.attributionControl.removeAttribution(layer.getAttribution()); - } - - if (layer.getEvents) { - this.off(layer.getEvents(), layer); - } - - delete this._layers[id]; - - if (this._loaded) { - this.fire('layerremove', {layer: layer}); - layer.fire('remove'); - } - - layer._map = layer._mapToAdd = null; - - return this; - }, - - hasLayer: function (layer) { - return !!layer && (L.stamp(layer) in this._layers); - }, - - eachLayer: function (method, context) { - for (var i in this._layers) { - method.call(context, this._layers[i]); - } - return this; - }, - - _addLayers: function (layers) { - layers = layers ? (L.Util.isArray(layers) ? layers : [layers]) : []; - - for (var i = 0, len = layers.length; i < len; i++) { - this.addLayer(layers[i]); - } - }, - - _addZoomLimit: function (layer) { - if (isNaN(layer.options.maxZoom) || !isNaN(layer.options.minZoom)) { - this._zoomBoundLayers[L.stamp(layer)] = layer; - this._updateZoomLevels(); - } - }, - - _removeZoomLimit: function (layer) { - var id = L.stamp(layer); - - if (this._zoomBoundLayers[id]) { - delete this._zoomBoundLayers[id]; - this._updateZoomLevels(); - } - }, - - _updateZoomLevels: function () { - var minZoom = Infinity, - maxZoom = -Infinity, - oldZoomSpan = this._getZoomSpan(); - - for (var i in this._zoomBoundLayers) { - var options = this._zoomBoundLayers[i].options; - - minZoom = options.minZoom === undefined ? minZoom : Math.min(minZoom, options.minZoom); - maxZoom = options.maxZoom === undefined ? maxZoom : Math.max(maxZoom, options.maxZoom); - } - - this._layersMaxZoom = maxZoom === -Infinity ? undefined : maxZoom; - this._layersMinZoom = minZoom === Infinity ? undefined : minZoom; - - if (oldZoomSpan !== this._getZoomSpan()) { - this.fire('zoomlevelschange'); - } - } -}); - - /* * Mercator projection that takes into account that the Earth is not a perfect sphere. * Less popular than spherical mercator; used by projections like EPSG:3395. */ L.Projection.Mercator = { - R: 6378137, - R_MINOR: 6356752.314245179, + MAX_LATITUDE: 85.0840591556, - bounds: L.bounds([-20037508.34279, -15496570.73972], [20037508.34279, 18764656.23138]), - - project: function (latlng) { - var d = Math.PI / 180, - r = this.R, - y = latlng.lat * d, - tmp = this.R_MINOR / r, - e = Math.sqrt(1 - tmp * tmp), - con = e * Math.sin(y); - - var ts = Math.tan(Math.PI / 4 - y / 2) / Math.pow((1 - con) / (1 + con), e / 2); - y = -r * Math.log(Math.max(ts, 1E-10)); - - return new L.Point(latlng.lng * d * r, y); - }, - - unproject: function (point) { - var d = 180 / Math.PI, - r = this.R, - tmp = this.R_MINOR / r, - e = Math.sqrt(1 - tmp * tmp), - ts = Math.exp(-point.y / r), - phi = Math.PI / 2 - 2 * Math.atan(ts); - - for (var i = 0, dphi = 0.1, con; i < 15 && Math.abs(dphi) > 1e-7; i++) { - con = e * Math.sin(phi); - con = Math.pow((1 - con) / (1 + con), e / 2); - dphi = Math.PI / 2 - 2 * Math.atan(ts * con) - phi; + R_MINOR: 6356752.314245179, + R_MAJOR: 6378137, + + project: function (latlng) { // (LatLng) -> Point + var d = L.LatLng.DEG_TO_RAD, + max = this.MAX_LATITUDE, + lat = Math.max(Math.min(max, latlng.lat), -max), + r = this.R_MAJOR, + r2 = this.R_MINOR, + x = latlng.lng * d * r, + y = lat * d, + tmp = r2 / r, + eccent = Math.sqrt(1.0 - tmp * tmp), + con = eccent * Math.sin(y); + + con = Math.pow((1 - con) / (1 + con), eccent * 0.5); + + var ts = Math.tan(0.5 * ((Math.PI * 0.5) - y)) / con; + y = -r * Math.log(ts); + + return new L.Point(x, y); + }, + + unproject: function (point) { // (Point, Boolean) -> LatLng + var d = L.LatLng.RAD_TO_DEG, + r = this.R_MAJOR, + r2 = this.R_MINOR, + lng = point.x * d / r, + tmp = r2 / r, + eccent = Math.sqrt(1 - (tmp * tmp)), + ts = Math.exp(- point.y / r), + phi = (Math.PI / 2) - 2 * Math.atan(ts), + numIter = 15, + tol = 1e-7, + i = numIter, + dphi = 0.1, + con; + + while ((Math.abs(dphi) > tol) && (--i > 0)) { + con = eccent * Math.sin(phi); + dphi = (Math.PI / 2) - 2 * Math.atan(ts * + Math.pow((1.0 - con) / (1.0 + con), 0.5 * eccent)) - phi; phi += dphi; } - return new L.LatLng(phi * d, point.x * d / r); + return new L.LatLng(phi * d, lng); } }; -/* - * L.CRS.EPSG3857 (World Mercator) CRS implementation. - */ -L.CRS.EPSG3395 = L.extend({}, L.CRS.Earth, { +L.CRS.EPSG3395 = L.extend({}, L.CRS, { code: 'EPSG:3395', + projection: L.Projection.Mercator, transformation: (function () { - var scale = 0.5 / (Math.PI * L.Projection.Mercator.R); + var m = L.Projection.Mercator, + r = m.R_MAJOR, + scale = 0.5 / (Math.PI * r); + return new L.Transformation(scale, 0.5, -scale, 0.5); }()) }); -/* - * L.GridLayer is used as base class for grid-like layers like TileLayer. - */ - -L.GridLayer = L.Layer.extend({ - - options: { - pane: 'tilePane', - - tileSize: 256, - opacity: 1, - - unloadInvisibleTiles: L.Browser.mobile, - updateWhenIdle: L.Browser.mobile, - updateInterval: 150 - - /* - minZoom: , - maxZoom: , - attribution: , - zIndex: , - bounds: - */ - }, - - initialize: function (options) { - options = L.setOptions(this, options); - }, - - onAdd: function () { - this._initContainer(); - - if (!this.options.updateWhenIdle) { - // update tiles on move, but not more often than once per given interval - this._update = L.Util.throttle(this._update, this.options.updateInterval, this); - } - - this._reset(); - this._update(); - }, - - beforeAdd: function (map) { - map._addZoomLimit(this); - }, - - onRemove: function (map) { - this._clearBgBuffer(); - L.DomUtil.remove(this._container); - - map._removeZoomLimit(this); - - this._container = null; - }, - - bringToFront: function () { - if (this._map) { - L.DomUtil.toFront(this._container); - this._setAutoZIndex(Math.max); - } - return this; - }, - - bringToBack: function () { - if (this._map) { - L.DomUtil.toBack(this._container); - this._setAutoZIndex(Math.min); - } - return this; - }, - - getAttribution: function () { - return this.options.attribution; - }, - - getContainer: function () { - return this._container; - }, - - setOpacity: function (opacity) { - this.options.opacity = opacity; - - if (this._map) { - this._updateOpacity(); - } - return this; - }, - - setZIndex: function (zIndex) { - this.options.zIndex = zIndex; - this._updateZIndex(); - - return this; - }, - - redraw: function () { - if (this._map) { - this._reset({hard: true}); - this._update(); - } - return this; - }, - - getEvents: function () { - var events = { - viewreset: this._reset, - moveend: this._update - }; - - if (!this.options.updateWhenIdle) { - events.move = this._update; - } - - if (this._zoomAnimated) { - events.zoomstart = this._startZoomAnim; - events.zoomanim = this._animateZoom; - events.zoomend = this._endZoomAnim; - } - - return events; - }, - - _updateZIndex: function () { - if (this._container && this.options.zIndex !== undefined) { - this._container.style.zIndex = this.options.zIndex; - } - }, - - _setAutoZIndex: function (compare) { - // go through all other layers of the same pane, set zIndex to max + 1 (front) or min - 1 (back) - - var layers = this.getPane().children, - edgeZIndex = -compare(-Infinity, Infinity); // -Infinity for max, Infinity for min - - for (var i = 0, len = layers.length, zIndex; i < len; i++) { - - zIndex = layers[i].style.zIndex; - - if (layers[i] !== this._container && zIndex) { - edgeZIndex = compare(edgeZIndex, +zIndex); - } - } - - if (isFinite(edgeZIndex)) { - this.options.zIndex = edgeZIndex + compare(-1, 1); - this._updateZIndex(); - } - }, - - _updateOpacity: function () { - var opacity = this.options.opacity; - - if (L.Browser.ielt9) { - // IE doesn't inherit filter opacity properly, so we're forced to set it on tiles - for (var i in this._tiles) { - L.DomUtil.setOpacity(this._tiles[i], opacity); - } - } else { - L.DomUtil.setOpacity(this._container, opacity); - } - }, - - _initContainer: function () { - if (this._container) { return; } - - this._container = L.DomUtil.create('div', 'leaflet-layer'); - this._updateZIndex(); - - if (this._zoomAnimated) { - var className = 'leaflet-tile-container leaflet-zoom-animated'; - - this._bgBuffer = L.DomUtil.create('div', className, this._container); - this._tileContainer = L.DomUtil.create('div', className, this._container); - - L.DomUtil.setTransform(this._tileContainer); - - } else { - this._tileContainer = this._container; - } - - if (this.options.opacity < 1) { - this._updateOpacity(); - } - - this.getPane().appendChild(this._container); - }, - - _reset: function (e) { - for (var key in this._tiles) { - this.fire('tileunload', { - tile: this._tiles[key] - }); - } - - if (this._abortLoading) { - this._abortLoading(); - } - - this._tiles = {}; - this._tilesToLoad = 0; - this._tilesTotal = 0; - - L.DomUtil.empty(this._tileContainer); - - if (this._zoomAnimated && e && e.hard) { - this._clearBgBuffer(); - } - - this._tileNumBounds = this._getTileNumBounds(); - this._resetWrap(); - }, - - _resetWrap: function () { - var map = this._map, - crs = map.options.crs; - - if (crs.infinite) { return; } - - var tileSize = this._getTileSize(); - - if (crs.wrapLng) { - this._wrapLng = [ - Math.floor(map.project([0, crs.wrapLng[0]]).x / tileSize), - Math.ceil(map.project([0, crs.wrapLng[1]]).x / tileSize) - ]; - } - - if (crs.wrapLat) { - this._wrapLat = [ - Math.floor(map.project([crs.wrapLat[0], 0]).y / tileSize), - Math.ceil(map.project([crs.wrapLat[1], 0]).y / tileSize) - ]; - } - }, - - _getTileSize: function () { - return this.options.tileSize; - }, - - _update: function () { - - if (!this._map) { return; } - - var bounds = this._map.getPixelBounds(), - zoom = this._map.getZoom(), - tileSize = this._getTileSize(); - - if (zoom > this.options.maxZoom || - zoom < this.options.minZoom) { - this._clearBgBuffer(); - return; - } - - // tile coordinates range for the current view - var tileBounds = L.bounds( - bounds.min.divideBy(tileSize).floor(), - bounds.max.divideBy(tileSize).floor()); - - this._addTiles(tileBounds); - - if (this.options.unloadInvisibleTiles) { - this._removeOtherTiles(tileBounds); - } - }, - - _addTiles: function (bounds) { - var queue = [], - center = bounds.getCenter(), - zoom = this._map.getZoom(); - - var j, i, coords; - - // create a queue of coordinates to load tiles from - for (j = bounds.min.y; j <= bounds.max.y; j++) { - for (i = bounds.min.x; i <= bounds.max.x; i++) { - - coords = new L.Point(i, j); - coords.z = zoom; - - // add tile to queue if it's not in cache or out of bounds - if (!(this._tileCoordsToKey(coords) in this._tiles) && this._isValidTile(coords)) { - queue.push(coords); - } - } - } - - var tilesToLoad = queue.length; - - if (tilesToLoad === 0) { return; } - - // if its the first batch of tiles to load - if (!this._tilesToLoad) { - this.fire('loading'); - } - - this._tilesToLoad += tilesToLoad; - this._tilesTotal += tilesToLoad; - - // sort tile queue to load tiles in order of their distance to center - queue.sort(function (a, b) { - return a.distanceTo(center) - b.distanceTo(center); - }); - - // create DOM fragment to append tiles in one batch - var fragment = document.createDocumentFragment(); - - for (i = 0; i < tilesToLoad; i++) { - this._addTile(queue[i], fragment); - } - - this._tileContainer.appendChild(fragment); - }, - - _isValidTile: function (coords) { - var crs = this._map.options.crs; - - if (!crs.infinite) { - // don't load tile if it's out of bounds and not wrapped - var bounds = this._tileNumBounds; - if ((!crs.wrapLng && (coords.x < bounds.min.x || coords.x > bounds.max.x)) || - (!crs.wrapLat && (coords.y < bounds.min.y || coords.y > bounds.max.y))) { return false; } - } - - if (!this.options.bounds) { return true; } - - // don't load tile if it doesn't intersect the bounds in options - var tileBounds = this._tileCoordsToBounds(coords); - return L.latLngBounds(this.options.bounds).intersects(tileBounds); - }, - - // converts tile coordinates to its geographical bounds - _tileCoordsToBounds: function (coords) { - - var map = this._map, - tileSize = this._getTileSize(), - - nwPoint = coords.multiplyBy(tileSize), - sePoint = nwPoint.add([tileSize, tileSize]), - - nw = map.wrapLatLng(map.unproject(nwPoint, coords.z)), - se = map.wrapLatLng(map.unproject(sePoint, coords.z)); - - return new L.LatLngBounds(nw, se); - }, - - // converts tile coordinates to key for the tile cache - _tileCoordsToKey: function (coords) { - return coords.x + ':' + coords.y; - }, - - // converts tile cache key to coordinates - _keyToTileCoords: function (key) { - var kArr = key.split(':'), - x = parseInt(kArr[0], 10), - y = parseInt(kArr[1], 10); - - return new L.Point(x, y); - }, - - // remove any present tiles that are off the specified bounds - _removeOtherTiles: function (bounds) { - for (var key in this._tiles) { - if (!bounds.contains(this._keyToTileCoords(key))) { - this._removeTile(key); - } - } - }, - - _removeTile: function (key) { - var tile = this._tiles[key]; - - L.DomUtil.remove(tile); - - delete this._tiles[key]; - - this.fire('tileunload', {tile: tile}); - }, - - _initTile: function (tile) { - var size = this._getTileSize(); - - L.DomUtil.addClass(tile, 'leaflet-tile'); - - tile.style.width = size + 'px'; - tile.style.height = size + 'px'; - - tile.onselectstart = L.Util.falseFn; - tile.onmousemove = L.Util.falseFn; - - // update opacity on tiles in IE7-8 because of filter inheritance problems - if (L.Browser.ielt9 && this.options.opacity < 1) { - L.DomUtil.setOpacity(tile, this.options.opacity); - } - - // without this hack, tiles disappear after zoom on Chrome for Android - // https://github.com/Leaflet/Leaflet/issues/2078 - if (L.Browser.android && !L.Browser.android23) { - tile.style.WebkitBackfaceVisibility = 'hidden'; - } - }, - - _addTile: function (coords, container) { - var tilePos = this._getTilePos(coords); - - // wrap tile coords if necessary (depending on CRS) - this._wrapCoords(coords); - - var tile = this.createTile(coords, L.bind(this._tileReady, this)); - - this._initTile(tile); - - // if createTile is defined with a second argument ("done" callback), - // we know that tile is async and will be ready later; otherwise - if (this.createTile.length < 2) { - // mark tile as ready, but delay one frame for opacity animation to happen - setTimeout(L.bind(this._tileReady, this, null, tile), 0); - } - - // we prefer top/left over translate3d so that we don't create a HW-accelerated layer from each tile - // which is slow, and it also fixes gaps between tiles in Safari - L.DomUtil.setPosition(tile, tilePos, true); - - // save tile in cache - this._tiles[this._tileCoordsToKey(coords)] = tile; - - container.appendChild(tile); - this.fire('tileloadstart', {tile: tile}); - }, - - _tileReady: function (err, tile) { - if (err) { - this.fire('tileerror', { - error: err, - tile: tile - }); - } - - L.DomUtil.addClass(tile, 'leaflet-tile-loaded'); - - this.fire('tileload', {tile: tile}); - - this._tilesToLoad--; - - if (this._tilesToLoad === 0) { - this._visibleTilesReady(); - } - }, - - _visibleTilesReady: function () { - this.fire('load'); - - if (this._zoomAnimated) { - // clear scaled tiles after all new tiles are loaded (for performance) - clearTimeout(this._clearBgBufferTimer); - this._clearBgBufferTimer = setTimeout(L.bind(this._clearBgBuffer, this), 300); - } - }, - - _getTilePos: function (coords) { - return coords - .multiplyBy(this._getTileSize()) - .subtract(this._map.getPixelOrigin()); - }, - - _wrapCoords: function (coords) { - coords.x = this._wrapLng ? L.Util.wrapNum(coords.x, this._wrapLng) : coords.x; - coords.y = this._wrapLat ? L.Util.wrapNum(coords.y, this._wrapLat) : coords.y; - }, - - // get the global tile coordinates range for the current zoom - _getTileNumBounds: function () { - var bounds = this._map.getPixelWorldBounds(), - size = this._getTileSize(); - - return bounds ? L.bounds( - bounds.min.divideBy(size).floor(), - bounds.max.divideBy(size).ceil().subtract([1, 1])) : null; - }, - - _startZoomAnim: function () { - this._prepareBgBuffer(); - this._prevTranslate = this._translate || new L.Point(0, 0); - this._prevScale = this._scale; - }, - - _animateZoom: function (e) { - // avoid stacking transforms by calculating cumulating translate/scale sequence - this._translate = this._prevTranslate.multiplyBy(e.scale).add(e.origin.multiplyBy(1 - e.scale)); - this._scale = this._prevScale * e.scale; - - L.DomUtil.setTransform(this._bgBuffer, this._translate, this._scale); - }, - - _endZoomAnim: function () { - var front = this._tileContainer; - front.style.visibility = ''; - L.DomUtil.toFront(front); // bring to front - }, - - _clearBgBuffer: function () { - var map = this._map, - bg = this._bgBuffer; - - if (map && !map._animatingZoom && !map.touchZoom._zooming && bg) { - L.DomUtil.empty(bg); - L.DomUtil.setTransform(bg); - } - }, - - _prepareBgBuffer: function () { - - var front = this._tileContainer, - bg = this._bgBuffer; - - if (this._abortLoading) { - this._abortLoading(); - } - - if (this._tilesToLoad / this._tilesTotal > 0.5) { - // if foreground layer doesn't have many tiles loaded, - // keep the existing bg layer and just zoom it some more - front.style.visibility = 'hidden'; - return; - } - - // prepare the buffer to become the front tile pane - bg.style.visibility = 'hidden'; - L.DomUtil.setTransform(bg); - - // switch out the current layer to be the new bg layer (and vice-versa) - this._tileContainer = bg; - this._bgBuffer = front; - - // reset bg layer transform info - this._translate = new L.Point(0, 0); - this._scale = 1; - - // prevent bg buffer from clearing right after zoom - clearTimeout(this._clearBgBufferTimer); - } -}); - -L.gridLayer = function (options) { - return new L.GridLayer(options); -}; - - /* * L.TileLayer is used for standard xyz-numbered tile layers. */ -L.TileLayer = L.GridLayer.extend({ +L.TileLayer = L.Class.extend({ + includes: L.Mixin.Events, options: { minZoom: 0, maxZoom: 18, - + tileSize: 256, subdomains: 'abc', - // errorTileUrl: '', - zoomOffset: 0 - + errorTileUrl: '', + attribution: '', + zoomOffset: 0, + opacity: 1, /* - maxNativeZoom: , - tms: , - zoomReverse: , - detectRetina: , - crossOrigin: , + maxNativeZoom: null, + zIndex: null, + tms: false, + continuousWorld: false, + noWrap: false, + zoomReverse: false, + detectRetina: false, + reuseTiles: false, + bounds: false, */ + unloadInvisibleTiles: L.Browser.mobile, + updateWhenIdle: L.Browser.mobile }, initialize: function (url, options) { - - this._url = url; - options = L.setOptions(this, options); // detecting retina displays, adjusting tileSize and zoom levels @@ -3039,13 +2460,127 @@ L.TileLayer = L.GridLayer.extend({ options.tileSize = Math.floor(options.tileSize / 2); options.zoomOffset++; - options.minZoom = Math.max(0, options.minZoom); - options.maxZoom--; + if (options.minZoom > 0) { + options.minZoom--; + } + this.options.maxZoom--; } - if (typeof options.subdomains === 'string') { - options.subdomains = options.subdomains.split(''); + if (options.bounds) { + options.bounds = L.latLngBounds(options.bounds); } + + this._url = url; + + var subdomains = this.options.subdomains; + + if (typeof subdomains === 'string') { + this.options.subdomains = subdomains.split(''); + } + }, + + onAdd: function (map) { + this._map = map; + this._animated = map._zoomAnimated; + + // create a container div for tiles + this._initContainer(); + + // set up events + map.on({ + 'viewreset': this._reset, + 'moveend': this._update + }, this); + + if (this._animated) { + map.on({ + 'zoomanim': this._animateZoom, + 'zoomend': this._endZoomAnim + }, this); + } + + if (!this.options.updateWhenIdle) { + this._limitedUpdate = L.Util.limitExecByInterval(this._update, 150, this); + map.on('move', this._limitedUpdate, this); + } + + this._reset(); + this._update(); + }, + + addTo: function (map) { + map.addLayer(this); + return this; + }, + + onRemove: function (map) { + this._container.parentNode.removeChild(this._container); + + map.off({ + 'viewreset': this._reset, + 'moveend': this._update + }, this); + + if (this._animated) { + map.off({ + 'zoomanim': this._animateZoom, + 'zoomend': this._endZoomAnim + }, this); + } + + if (!this.options.updateWhenIdle) { + map.off('move', this._limitedUpdate, this); + } + + this._container = null; + this._map = null; + }, + + bringToFront: function () { + var pane = this._map._panes.tilePane; + + if (this._container) { + pane.appendChild(this._container); + this._setAutoZIndex(pane, Math.max); + } + + return this; + }, + + bringToBack: function () { + var pane = this._map._panes.tilePane; + + if (this._container) { + pane.insertBefore(this._container, pane.firstChild); + this._setAutoZIndex(pane, Math.min); + } + + return this; + }, + + getAttribution: function () { + return this.options.attribution; + }, + + getContainer: function () { + return this._container; + }, + + setOpacity: function (opacity) { + this.options.opacity = opacity; + + if (this._map) { + this._updateOpacity(); + } + + return this; + }, + + setZIndex: function (zIndex) { + this.options.zIndex = zIndex; + this._updateZIndex(); + + return this; }, setUrl: function (url, noRedraw) { @@ -3054,74 +2589,275 @@ L.TileLayer = L.GridLayer.extend({ if (!noRedraw) { this.redraw(); } + return this; }, - createTile: function (coords, done) { - var tile = document.createElement('img'); + redraw: function () { + if (this._map) { + this._reset({hard: true}); + this._update(); + } + return this; + }, - tile.onload = L.bind(this._tileOnLoad, this, done, tile); - tile.onerror = L.bind(this._tileOnError, this, done, tile); - - if (this.options.crossOrigin) { - tile.crossOrigin = ''; + _updateZIndex: function () { + if (this._container && this.options.zIndex !== undefined) { + this._container.style.zIndex = this.options.zIndex; } - - /* - Alt tag is set to empty string to keep screen readers from reading URL and for compliance reasons - http://www.w3.org/TR/WCAG20-TECHS/H67 - */ - tile.alt = ''; + }, - tile.src = this.getTileUrl(coords); + _setAutoZIndex: function (pane, compare) { - return tile; + var layers = pane.children, + edgeZIndex = -compare(Infinity, -Infinity), // -Infinity for max, Infinity for min + zIndex, i, len; + + for (i = 0, len = layers.length; i < len; i++) { + + if (layers[i] !== this._container) { + zIndex = parseInt(layers[i].style.zIndex, 10); + + if (!isNaN(zIndex)) { + edgeZIndex = compare(edgeZIndex, zIndex); + } + } + } + + this.options.zIndex = this._container.style.zIndex = + (isFinite(edgeZIndex) ? edgeZIndex : 0) + compare(1, -1); }, - getTileUrl: function (coords) { - return L.Util.template(this._url, L.extend({ - r: this.options.detectRetina && L.Browser.retina && this.options.maxZoom > 0 ? '@2x' : '', - s: this._getSubdomain(coords), - x: coords.x, - y: this.options.tms ? this._tileNumBounds.max.y - coords.y : coords.y, - z: this._getZoomForUrl() - }, this.options)); + _updateOpacity: function () { + var i, + tiles = this._tiles; + + if (L.Browser.ielt9) { + for (i in tiles) { + L.DomUtil.setOpacity(tiles[i], this.options.opacity); + } + } else { + L.DomUtil.setOpacity(this._container, this.options.opacity); + } }, - _tileOnLoad: function (done, tile) { - done(null, tile); + _initContainer: function () { + var tilePane = this._map._panes.tilePane; + + if (!this._container) { + this._container = L.DomUtil.create('div', 'leaflet-layer'); + + this._updateZIndex(); + + if (this._animated) { + var className = 'leaflet-tile-container'; + + this._bgBuffer = L.DomUtil.create('div', className, this._container); + this._tileContainer = L.DomUtil.create('div', className, this._container); + + } else { + this._tileContainer = this._container; + } + + tilePane.appendChild(this._container); + + if (this.options.opacity < 1) { + this._updateOpacity(); + } + } }, - _tileOnError: function (done, tile, e) { - var errorUrl = this.options.errorTileUrl; - if (errorUrl) { - tile.src = errorUrl; + _reset: function (e) { + for (var key in this._tiles) { + this.fire('tileunload', {tile: this._tiles[key]}); + } + + this._tiles = {}; + this._tilesToLoad = 0; + + if (this.options.reuseTiles) { + this._unusedTiles = []; + } + + this._tileContainer.innerHTML = ''; + + if (this._animated && e && e.hard) { + this._clearBgBuffer(); } - done(e, tile); + + this._initContainer(); }, _getTileSize: function () { var map = this._map, - options = this.options, - zoom = map.getZoom() + options.zoomOffset, - zoomN = options.maxNativeZoom; + zoom = map.getZoom() + this.options.zoomOffset, + zoomN = this.options.maxNativeZoom, + tileSize = this.options.tileSize; + + if (zoomN && zoom > zoomN) { + tileSize = Math.round(map.getZoomScale(zoom) / map.getZoomScale(zoomN) * tileSize); + } + + return tileSize; + }, + + _update: function () { - // increase tile size when overscaling - return zoomN && zoom > zoomN ? - Math.round(map.getZoomScale(zoom) / map.getZoomScale(zoomN) * options.tileSize) : - options.tileSize; + if (!this._map) { return; } + + var map = this._map, + bounds = map.getPixelBounds(), + zoom = map.getZoom(), + tileSize = this._getTileSize(); + + if (zoom > this.options.maxZoom || zoom < this.options.minZoom) { + return; + } + + var tileBounds = L.bounds( + bounds.min.divideBy(tileSize)._floor(), + bounds.max.divideBy(tileSize)._floor()); + + this._addTilesFromCenterOut(tileBounds); + + if (this.options.unloadInvisibleTiles || this.options.reuseTiles) { + this._removeOtherTiles(tileBounds); + } + }, + + _addTilesFromCenterOut: function (bounds) { + var queue = [], + center = bounds.getCenter(); + + var j, i, point; + + for (j = bounds.min.y; j <= bounds.max.y; j++) { + for (i = bounds.min.x; i <= bounds.max.x; i++) { + point = new L.Point(i, j); + + if (this._tileShouldBeLoaded(point)) { + queue.push(point); + } + } + } + + var tilesToLoad = queue.length; + + if (tilesToLoad === 0) { return; } + + // load tiles in order of their distance to center + queue.sort(function (a, b) { + return a.distanceTo(center) - b.distanceTo(center); + }); + + var fragment = document.createDocumentFragment(); + + // if its the first batch of tiles to load + if (!this._tilesToLoad) { + this.fire('loading'); + } + + this._tilesToLoad += tilesToLoad; + + for (i = 0; i < tilesToLoad; i++) { + this._addTile(queue[i], fragment); + } + + this._tileContainer.appendChild(fragment); + }, + + _tileShouldBeLoaded: function (tilePoint) { + if ((tilePoint.x + ':' + tilePoint.y) in this._tiles) { + return false; // already loaded + } + + var options = this.options; + + if (!options.continuousWorld) { + var limit = this._getWrapTileNum(); + + // don't load if exceeds world bounds + if ((options.noWrap && (tilePoint.x < 0 || tilePoint.x >= limit.x)) || + tilePoint.y < 0 || tilePoint.y >= limit.y) { return false; } + } + + if (options.bounds) { + var tileSize = options.tileSize, + nwPoint = tilePoint.multiplyBy(tileSize), + sePoint = nwPoint.add([tileSize, tileSize]), + nw = this._map.unproject(nwPoint), + se = this._map.unproject(sePoint); + + // TODO temporary hack, will be removed after refactoring projections + // https://github.com/Leaflet/Leaflet/issues/1618 + if (!options.continuousWorld && !options.noWrap) { + nw = nw.wrap(); + se = se.wrap(); + } + + if (!options.bounds.intersects([nw, se])) { return false; } + } + + return true; + }, + + _removeOtherTiles: function (bounds) { + var kArr, x, y, key; + + for (key in this._tiles) { + kArr = key.split(':'); + x = parseInt(kArr[0], 10); + y = parseInt(kArr[1], 10); + + // remove tile if it's out of bounds + if (x < bounds.min.x || x > bounds.max.x || y < bounds.min.y || y > bounds.max.y) { + this._removeTile(key); + } + } }, _removeTile: function (key) { var tile = this._tiles[key]; - L.GridLayer.prototype._removeTile.call(this, key); + this.fire('tileunload', {tile: tile, url: tile.src}); + + if (this.options.reuseTiles) { + L.DomUtil.removeClass(tile, 'leaflet-tile-loaded'); + this._unusedTiles.push(tile); + + } else if (tile.parentNode === this._tileContainer) { + this._tileContainer.removeChild(tile); + } - // for https://github.com/Leaflet/Leaflet/issues/137 + // for https://github.com/CloudMade/Leaflet/issues/137 if (!L.Browser.android) { tile.onload = null; tile.src = L.Util.emptyImageUrl; } + + delete this._tiles[key]; + }, + + _addTile: function (tilePoint, container) { + var tilePos = this._getTilePos(tilePoint); + + // get unused tile - or create a new tile + var tile = this._getTile(); + + /* + Chrome 20 layouts much faster with top/left (verify with timeline, frames) + Android 4 browser has display issues with top/left and requires transform instead + (other browsers don't currently care) - see debug/hacks/jitter.html for an example + */ + L.DomUtil.setPosition(tile, tilePos, L.Browser.chrome); + + this._tiles[tilePoint.x + ':' + tilePoint.y] = tile; + + this._loadTile(tile, tilePoint); + + if (tile.parentNode !== this._tileContainer) { + container.appendChild(tile); + } }, _getZoomForUrl: function () { @@ -3138,25 +2874,143 @@ L.TileLayer = L.GridLayer.extend({ return options.maxNativeZoom ? Math.min(zoom, options.maxNativeZoom) : zoom; }, + _getTilePos: function (tilePoint) { + var origin = this._map.getPixelOrigin(), + tileSize = this._getTileSize(); + + return tilePoint.multiplyBy(tileSize).subtract(origin); + }, + + // image-specific code (override to implement e.g. Canvas or SVG tile layer) + + getTileUrl: function (tilePoint) { + return L.Util.template(this._url, L.extend({ + s: this._getSubdomain(tilePoint), + z: tilePoint.z, + x: tilePoint.x, + y: tilePoint.y + }, this.options)); + }, + + _getWrapTileNum: function () { + var crs = this._map.options.crs, + size = crs.getSize(this._map.getZoom()); + return size.divideBy(this._getTileSize())._floor(); + }, + + _adjustTilePoint: function (tilePoint) { + + var limit = this._getWrapTileNum(); + + // wrap tile coordinates + if (!this.options.continuousWorld && !this.options.noWrap) { + tilePoint.x = ((tilePoint.x % limit.x) + limit.x) % limit.x; + } + + if (this.options.tms) { + tilePoint.y = limit.y - tilePoint.y - 1; + } + + tilePoint.z = this._getZoomForUrl(); + }, + _getSubdomain: function (tilePoint) { var index = Math.abs(tilePoint.x + tilePoint.y) % this.options.subdomains.length; return this.options.subdomains[index]; }, - // stops loading all tiles in the background layer - _abortLoading: function () { - var i, tile; - for (i in this._tiles) { - tile = this._tiles[i]; + _getTile: function () { + if (this.options.reuseTiles && this._unusedTiles.length > 0) { + var tile = this._unusedTiles.pop(); + this._resetTile(tile); + return tile; + } + return this._createTile(); + }, + + // Override if data stored on a tile needs to be cleaned up before reuse + _resetTile: function (/*tile*/) {}, + + _createTile: function () { + var tile = L.DomUtil.create('img', 'leaflet-tile'); + tile.style.width = tile.style.height = this._getTileSize() + 'px'; + tile.galleryimg = 'no'; + + tile.onselectstart = tile.onmousemove = L.Util.falseFn; + + if (L.Browser.ielt9 && this.options.opacity !== undefined) { + L.DomUtil.setOpacity(tile, this.options.opacity); + } + // without this hack, tiles disappear after zoom on Chrome for Android + // https://github.com/Leaflet/Leaflet/issues/2078 + if (L.Browser.mobileWebkit3d) { + tile.style.WebkitBackfaceVisibility = 'hidden'; + } + return tile; + }, + + _loadTile: function (tile, tilePoint) { + tile._layer = this; + tile.onload = this._tileOnLoad; + tile.onerror = this._tileOnError; + + this._adjustTilePoint(tilePoint); + tile.src = this.getTileUrl(tilePoint); + + this.fire('tileloadstart', { + tile: tile, + url: tile.src + }); + }, + + _tileLoaded: function () { + this._tilesToLoad--; + + if (this._animated) { + L.DomUtil.addClass(this._tileContainer, 'leaflet-zoom-animated'); + } - tile.onload = L.Util.falseFn; - tile.onerror = L.Util.falseFn; + if (!this._tilesToLoad) { + this.fire('load'); - if (!tile.complete) { - tile.src = L.Util.emptyImageUrl; - L.DomUtil.remove(tile); + if (this._animated) { + // clear scaled tiles after all new tiles are loaded (for performance) + clearTimeout(this._clearBgBufferTimer); + this._clearBgBufferTimer = setTimeout(L.bind(this._clearBgBuffer, this), 500); } } + }, + + _tileOnLoad: function () { + var layer = this._layer; + + //Only if we are loading an actual image + if (this.src !== L.Util.emptyImageUrl) { + L.DomUtil.addClass(this, 'leaflet-tile-loaded'); + + layer.fire('tileload', { + tile: this, + url: this.src + }); + } + + layer._tileLoaded(); + }, + + _tileOnError: function () { + var layer = this._layer; + + layer.fire('tileerror', { + tile: this, + url: this.src + }); + + var newUrl = layer.options.errorTileUrl; + if (newUrl) { + this.src = newUrl; + } + + layer._tileLoaded(); } }); @@ -3166,7 +3020,7 @@ L.tileLayer = function (url, options) { /* - * L.TileLayer.WMS is used for WMS tile layers. + * L.TileLayer.WMS is used for putting WMS tile layers on the map. */ L.TileLayer.WMS = L.TileLayer.extend({ @@ -3181,33 +3035,34 @@ L.TileLayer.WMS = L.TileLayer.extend({ transparent: false }, - initialize: function (url, options) { + initialize: function (url, options) { // (String, Object) this._url = url; - var wmsParams = L.extend({}, this.defaultWmsParams); + var wmsParams = L.extend({}, this.defaultWmsParams), + tileSize = options.tileSize || this.options.tileSize; + + if (options.detectRetina && L.Browser.retina) { + wmsParams.width = wmsParams.height = tileSize * 2; + } else { + wmsParams.width = wmsParams.height = tileSize; + } - // all keys that are not TileLayer options go to WMS params for (var i in options) { - if (!this.options.hasOwnProperty(i) && - i !== 'crs' && - i !== 'uppercase') { + // all keys that are not TileLayer options go to WMS params + if (!this.options.hasOwnProperty(i) && i !== 'crs') { wmsParams[i] = options[i]; } } - options = L.setOptions(this, options); - - wmsParams.width = wmsParams.height = - options.tileSize * (options.detectRetina && L.Browser.retina ? 2 : 1); - this.wmsParams = wmsParams; + + L.setOptions(this, options); }, onAdd: function (map) { this._crs = this.options.crs || map.options.crs; - this._uppercase = this.options.uppercase || false; this._wmsVersion = parseFloat(this.wmsParams.version); @@ -3217,21 +3072,23 @@ L.TileLayer.WMS = L.TileLayer.extend({ L.TileLayer.prototype.onAdd.call(this, map); }, - getTileUrl: function (coords) { + getTileUrl: function (tilePoint) { // (Point, Number) -> String + + var map = this._map, + tileSize = this.options.tileSize, - var tileBounds = this._tileCoordsToBounds(coords), - nw = this._crs.project(tileBounds.getNorthWest()), - se = this._crs.project(tileBounds.getSouthEast()), + nwPoint = tilePoint.multiplyBy(tileSize), + sePoint = nwPoint.add([tileSize, tileSize]), - bbox = (this._wmsVersion >= 1.3 && this._crs === L.CRS.EPSG4326 ? - [se.y, nw.x, nw.y, se.x] : - [nw.x, se.y, se.x, nw.y]).join(','), + nw = this._crs.project(map.unproject(nwPoint, tilePoint.z)), + se = this._crs.project(map.unproject(sePoint, tilePoint.z)), + bbox = this._wmsVersion >= 1.3 && this._crs === L.CRS.EPSG4326 ? + [se.y, nw.x, nw.y, se.x].join(',') : + [nw.x, se.y, se.x, nw.y].join(','), - url = L.TileLayer.prototype.getTileUrl.call(this, coords); + url = L.Util.template(this._url, {s: this._getSubdomain(tilePoint)}); - return url + - L.Util.getParamString(this.wmsParams, url, this._uppercase) + - (this._uppercase ? '&BBOX=' : '&bbox=') + bbox; + return url + L.Util.getParamString(this.wmsParams, url, true) + '&BBOX=' + bbox; }, setParams: function (params, noRedraw) { @@ -3252,14 +3109,77 @@ L.tileLayer.wms = function (url, options) { /* - * L.ImageOverlay is used to overlay images over the map (to specific geographical bounds). + * L.TileLayer.Canvas is a class that you can use as a base for creating + * dynamically drawn Canvas-based tile layers. */ -L.ImageOverlay = L.Layer.extend({ - +L.TileLayer.Canvas = L.TileLayer.extend({ options: { - opacity: 1, - alt: '' + async: false + }, + + initialize: function (options) { + L.setOptions(this, options); + }, + + redraw: function () { + if (this._map) { + this._reset({hard: true}); + this._update(); + } + + for (var i in this._tiles) { + this._redrawTile(this._tiles[i]); + } + return this; + }, + + _redrawTile: function (tile) { + this.drawTile(tile, tile._tilePoint, this._map._zoom); + }, + + _createTile: function () { + var tile = L.DomUtil.create('canvas', 'leaflet-tile'); + tile.width = tile.height = this.options.tileSize; + tile.onselectstart = tile.onmousemove = L.Util.falseFn; + return tile; + }, + + _loadTile: function (tile, tilePoint) { + tile._layer = this; + tile._tilePoint = tilePoint; + + this._redrawTile(tile); + + if (!this.options.async) { + this.tileDrawn(tile); + } + }, + + drawTile: function (/*tile, tilePoint*/) { + // override with rendering code + }, + + tileDrawn: function (tile) { + this._tileOnLoad.call(tile); + } +}); + + +L.tileLayer.canvas = function (options) { + return new L.TileLayer.Canvas(options); +}; + + +/* + * L.ImageOverlay is used to overlay images over the map (to specific geographical bounds). + */ + +L.ImageOverlay = L.Class.extend({ + includes: L.Mixin.Events, + + options: { + opacity: 1 }, initialize: function (url, bounds, options) { // (String, LatLngBounds, Object) @@ -3269,109 +3189,121 @@ L.ImageOverlay = L.Layer.extend({ L.setOptions(this, options); }, - onAdd: function () { + onAdd: function (map) { + this._map = map; + if (!this._image) { this._initImage(); - - if (this.options.opacity < 1) { - this._updateOpacity(); - } } - this.getPane().appendChild(this._image); + map._panes.overlayPane.appendChild(this._image); + + map.on('viewreset', this._reset, this); + + if (map.options.zoomAnimation && L.Browser.any3d) { + map.on('zoomanim', this._animateZoom, this); + } this._reset(); }, - onRemove: function () { - L.DomUtil.remove(this._image); + onRemove: function (map) { + map.getPanes().overlayPane.removeChild(this._image); + + map.off('viewreset', this._reset, this); + + if (map.options.zoomAnimation) { + map.off('zoomanim', this._animateZoom, this); + } + }, + + addTo: function (map) { + map.addLayer(this); + return this; }, setOpacity: function (opacity) { this.options.opacity = opacity; - - if (this._image) { - this._updateOpacity(); - } + this._updateOpacity(); return this; }, + // TODO remove bringToFront/bringToBack duplication from TileLayer/Path bringToFront: function () { - if (this._map) { - L.DomUtil.toFront(this._image); + if (this._image) { + this._map._panes.overlayPane.appendChild(this._image); } return this; }, bringToBack: function () { - if (this._map) { - L.DomUtil.toBack(this._image); + var pane = this._map._panes.overlayPane; + if (this._image) { + pane.insertBefore(this._image, pane.firstChild); } return this; }, setUrl: function (url) { this._url = url; - - if (this._image) { - this._image.src = url; - } - return this; + this._image.src = this._url; }, getAttribution: function () { return this.options.attribution; }, - getEvents: function () { - var events = { - viewreset: this._reset - }; + _initImage: function () { + this._image = L.DomUtil.create('img', 'leaflet-image-layer'); - if (this._zoomAnimated) { - events.zoomanim = this._animateZoom; + if (this._map.options.zoomAnimation && L.Browser.any3d) { + L.DomUtil.addClass(this._image, 'leaflet-zoom-animated'); + } else { + L.DomUtil.addClass(this._image, 'leaflet-zoom-hide'); } - return events; - }, - - getBounds: function() { - return this._bounds; - }, - - _initImage: function () { - var img = this._image = L.DomUtil.create('img', - 'leaflet-image-layer ' + (this._zoomAnimated ? 'leaflet-zoom-animated' : '')); - - img.onselectstart = L.Util.falseFn; - img.onmousemove = L.Util.falseFn; + this._updateOpacity(); - img.onload = L.bind(this.fire, this, 'load'); - img.src = this._url; - img.alt = this.options.alt; + //TODO createImage util method to remove duplication + L.extend(this._image, { + galleryimg: 'no', + onselectstart: L.Util.falseFn, + onmousemove: L.Util.falseFn, + onload: L.bind(this._onImageLoad, this), + src: this._url + }); }, _animateZoom: function (e) { - var topLeft = this._map._latLngToNewLayerPoint(this._bounds.getNorthWest(), e.zoom, e.center), - size = this._map._latLngToNewLayerPoint(this._bounds.getSouthEast(), e.zoom, e.center).subtract(topLeft), - offset = topLeft.add(size._multiplyBy((1 - 1 / e.scale) / 2)); + var map = this._map, + image = this._image, + scale = map.getZoomScale(e.zoom), + nw = this._bounds.getNorthWest(), + se = this._bounds.getSouthEast(), + + topLeft = map._latLngToNewLayerPoint(nw, e.zoom, e.center), + size = map._latLngToNewLayerPoint(se, e.zoom, e.center)._subtract(topLeft), + origin = topLeft._add(size._multiplyBy((1 / 2) * (1 - 1 / scale))); - L.DomUtil.setTransform(this._image, offset, e.scale); + image.style[L.DomUtil.TRANSFORM] = + L.DomUtil.getTranslateString(origin) + ' scale(' + scale + ') '; }, _reset: function () { - var image = this._image, - bounds = new L.Bounds( - this._map.latLngToLayerPoint(this._bounds.getNorthWest()), - this._map.latLngToLayerPoint(this._bounds.getSouthEast())), - size = bounds.getSize(); + var image = this._image, + topLeft = this._map.latLngToLayerPoint(this._bounds.getNorthWest()), + size = this._map.latLngToLayerPoint(this._bounds.getSouthEast())._subtract(topLeft); - L.DomUtil.setPosition(image, bounds.min); + L.DomUtil.setPosition(image, topLeft); image.style.width = size.x + 'px'; image.style.height = size.y + 'px'; }, + _onImageLoad: function () { + this.fire('load'); + }, + _updateOpacity: function () { L.DomUtil.setOpacity(this._image, this.options.opacity); } @@ -3387,8 +3319,8 @@ L.imageOverlay = function (url, bounds, options) { */ L.Icon = L.Class.extend({ - /* options: { + /* iconUrl: (String) (required) iconRetinaUrl: (String) (optional, used for retina devices if detected) iconSize: (Point) (can be set through CSS) @@ -3398,9 +3330,9 @@ L.Icon = L.Class.extend({ shadowRetinaUrl: (String) (optional, used for retina devices if detected) shadowSize: (Point) shadowAnchor: (Point) - className: (String) + */ + className: '' }, - */ initialize: function (options) { L.setOptions(this, options); @@ -3424,7 +3356,12 @@ L.Icon = L.Class.extend({ return null; } - var img = this._createImg(src, oldIcon && oldIcon.tagName === 'IMG' ? oldIcon : null); + var img; + if (!oldIcon || oldIcon.tagName !== 'IMG') { + img = this._createImg(src); + } else { + img = this._createImg(src, oldIcon); + } this._setIconStyles(img, name); return img; @@ -3433,10 +3370,19 @@ L.Icon = L.Class.extend({ _setIconStyles: function (img, name) { var options = this.options, size = L.point(options[name + 'Size']), - anchor = L.point(name === 'shadow' && options.shadowAnchor || options.iconAnchor || - size && size.divideBy(2, true)); + anchor; - img.className = 'leaflet-marker-' + name + ' ' + (options.className || ''); + if (name === 'shadow') { + anchor = L.point(options.shadowAnchor || options.iconAnchor); + } else { + anchor = L.point(options.iconAnchor); + } + + if (!anchor && size) { + anchor = size.divideBy(2, true); + } + + img.className = 'leaflet-marker-' + name + ' ' + options.className; if (anchor) { img.style.marginLeft = (-anchor.x) + 'px'; @@ -3456,7 +3402,10 @@ L.Icon = L.Class.extend({ }, _getIconUrl: function (name) { - return L.Browser.retina && this.options[name + 'RetinaUrl'] || this.options[name + 'Url']; + if (L.Browser.retina && this.options[name + 'RetinaUrl']) { + return this.options[name + 'RetinaUrl']; + } + return this.options[name + 'Url']; } }); @@ -3472,10 +3421,11 @@ L.icon = function (options) { L.Icon.Default = L.Icon.extend({ options: { - iconSize: [25, 41], - iconAnchor: [12, 41], + iconSize: [25, 41], + iconAnchor: [12, 41], popupAnchor: [1, -34], - shadowSize: [41, 41] + + shadowSize: [41, 41] }, _getIconUrl: function (name) { @@ -3485,13 +3435,17 @@ L.Icon.Default = L.Icon.extend({ return this.options[key]; } + if (L.Browser.retina && name === 'icon') { + name += '-2x'; + } + var path = L.Icon.Default.imagePath; if (!path) { throw new Error('Couldn\'t autodetect L.Icon.Default.imagePath, set it manually.'); } - return path + '/marker-' + name + (L.Browser.retina && name === 'icon' ? '-2x' : '') + '.png'; + return path + '/marker-' + name + '.png'; } }); @@ -3499,12 +3453,13 @@ L.Icon.Default.imagePath = (function () { var scripts = document.getElementsByTagName('script'), leafletRe = /[\/^]leaflet[\-\._]?([\w\-\._]*)\.js\??/; - var i, len, src, path; + var i, len, src, matches, path; for (i = 0, len = scripts.length; i < len; i++) { src = scripts[i].src; + matches = src.match(leafletRe); - if (src.match(leafletRe)) { + if (matches) { path = src.split(leafletRe)[0]; return (path ? path + '/' : '') + 'images'; } @@ -3516,20 +3471,20 @@ L.Icon.Default.imagePath = (function () { * L.Marker is used to display clickable/draggable icons on the map. */ -L.Marker = L.Layer.extend({ +L.Marker = L.Class.extend({ - options: { - pane: 'markerPane', + includes: L.Mixin.Events, + options: { icon: new L.Icon.Default(), - // title: '', - // alt: '', - interactive: true, - // draggable: false, + title: '', + alt: '', + clickable: true, + draggable: false, keyboard: true, zIndexOffset: 0, opacity: 1, - // riseOnHover: false, + riseOnHover: false, riseOffset: 250 }, @@ -3539,29 +3494,40 @@ L.Marker = L.Layer.extend({ }, onAdd: function (map) { - this._zoomAnimated = this._zoomAnimated && map.options.markerZoomAnimation; + this._map = map; + + map.on('viewreset', this.update, this); this._initIcon(); this.update(); + this.fire('add'); + + if (map.options.zoomAnimation && map.options.markerZoomAnimation) { + map.on('zoomanim', this._animateZoom, this); + } + }, + + addTo: function (map) { + map.addLayer(this); + return this; }, - onRemove: function () { + onRemove: function (map) { if (this.dragging) { this.dragging.disable(); } this._removeIcon(); this._removeShadow(); - }, - getEvents: function () { - var events = {viewreset: this.update}; + this.fire('remove'); - if (this._zoomAnimated) { - events.zoomanim = this._animateZoom; - } + map.off({ + 'viewreset': this.update, + 'zoomanim': this._animateZoom + }, this); - return events; + this._map = null; }, getLatLng: function () { @@ -3569,15 +3535,18 @@ L.Marker = L.Layer.extend({ }, setLatLng: function (latlng) { - var oldLatLng = this._latlng; this._latlng = L.latLng(latlng); + this.update(); - return this.fire('move', { oldLatLng: oldLatLng, latlng: this._latlng }); + + return this.fire('move', { latlng: this._latlng }); }, setZIndexOffset: function (offset) { this.options.zIndexOffset = offset; - return this.update(); + this.update(); + + return this; }, setIcon: function (icon) { @@ -3590,14 +3559,13 @@ L.Marker = L.Layer.extend({ } if (this._popup) { - this.bindPopup(this._popup, this._popup.options); + this.bindPopup(this._popup); } return this; }, update: function () { - if (this._icon) { var pos = this._map.latLngToLayerPoint(this._latlng).round(); this._setPos(pos); @@ -3608,7 +3576,9 @@ L.Marker = L.Layer.extend({ _initIcon: function () { var options = this.options, - classToAdd = 'leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide'); + map = this._map, + animation = (map.options.zoomAnimation && map.options.markerZoomAnimation), + classToAdd = animation ? 'leaflet-zoom-animated' : 'leaflet-zoom-hide'; var icon = options.icon.createIcon(this._icon), addIcon = false; @@ -3623,6 +3593,7 @@ L.Marker = L.Layer.extend({ if (options.title) { icon.title = options.title; } + if (options.alt) { icon.alt = options.alt; } @@ -3635,13 +3606,13 @@ L.Marker = L.Layer.extend({ } this._icon = icon; + this._initInteraction(); if (options.riseOnHover) { - L.DomEvent.on(icon, { - mouseover: this._bringToFront, - mouseout: this._resetZIndex - }, this); + L.DomEvent + .on(icon, 'mouseover', this._bringToFront, this) + .on(icon, 'mouseout', this._resetZIndex, this); } var newShadow = options.icon.createShadow(this._shadow), @@ -3663,30 +3634,32 @@ L.Marker = L.Layer.extend({ } + var panes = this._map._panes; + if (addIcon) { - this.getPane().appendChild(this._icon); + panes.markerPane.appendChild(this._icon); } + if (newShadow && addShadow) { - this.getPane('shadowPane').appendChild(this._shadow); + panes.shadowPane.appendChild(this._shadow); } }, _removeIcon: function () { if (this.options.riseOnHover) { - L.DomEvent.off(this._icon, { - mouseover: this._bringToFront, - mouseout: this._resetZIndex - }, this); + L.DomEvent + .off(this._icon, 'mouseover', this._bringToFront) + .off(this._icon, 'mouseout', this._resetZIndex); } - L.DomUtil.remove(this._icon); + this._map._panes.markerPane.removeChild(this._icon); this._icon = null; }, _removeShadow: function () { if (this._shadow) { - L.DomUtil.remove(this._shadow); + this._map._panes.shadowPane.removeChild(this._shadow); } this._shadow = null; }, @@ -3715,18 +3688,22 @@ L.Marker = L.Layer.extend({ _initInteraction: function () { - if (!this.options.interactive) { return; } + if (!this.options.clickable) { return; } + + // TODO refactor into something shared with Map/Path/etc. to DRY it up - L.DomUtil.addClass(this._icon, 'leaflet-interactive'); + var icon = this._icon, + events = ['dblclick', 'mousedown', 'mouseover', 'mouseout', 'contextmenu']; - L.DomEvent.on(this._icon, 'click dblclick mousedown mouseup mouseover mousemove mouseout contextmenu keypress', - this._fireMouseEvent, this); + L.DomUtil.addClass(icon, 'leaflet-clickable'); + L.DomEvent.on(icon, 'click', this._onMouseClick, this); + L.DomEvent.on(icon, 'keypress', this._onKeyPress, this); + + for (var i = 0; i < events.length; i++) { + L.DomEvent.on(icon, events[i], this._fireMouseEvent, this); + } if (L.Handler.MarkerDrag) { - if (this.dragging) { - this.dragging.disable(); - } - this.dragging = new L.Handler.MarkerDrag(this); if (this.options.draggable) { @@ -3735,18 +3712,48 @@ L.Marker = L.Layer.extend({ } }, - _fireMouseEvent: function (e, type) { - // to prevent outline when clicking on keyboard-focusable marker - if (e.type === 'mousedown') { - L.DomEvent.preventDefault(e); + _onMouseClick: function (e) { + var wasDragged = this.dragging && this.dragging.moved(); + + if (this.hasEventListeners(e.type) || wasDragged) { + L.DomEvent.stopPropagation(e); } - if (e.type === 'keypress' && e.keyCode === 13) { - type = 'click'; + if (wasDragged) { return; } + + if ((!this.dragging || !this.dragging._enabled) && this._map.dragging && this._map.dragging.moved()) { return; } + + this.fire(e.type, { + originalEvent: e, + latlng: this._latlng + }); + }, + + _onKeyPress: function (e) { + if (e.keyCode === 13) { + this.fire('click', { + originalEvent: e, + latlng: this._latlng + }); } + }, - if (this._map) { - this._map._fireMouseEvent(this, e, type, true, this._latlng); + _fireMouseEvent: function (e) { + + this.fire(e.type, { + originalEvent: e, + latlng: this._latlng + }); + + // TODO proper custom event propagation + // this line will always be called if marker is in a FeatureGroup + if (e.type === 'contextmenu' && this.hasEventListeners(e.type)) { + L.DomEvent.preventDefault(e); + } + if (e.type !== 'mousedown') { + L.DomEvent.stopPropagation(e); + } else { + L.DomEvent.preventDefault(e); } }, @@ -3760,12 +3767,9 @@ L.Marker = L.Layer.extend({ }, _updateOpacity: function () { - var opacity = this.options.opacity; - - L.DomUtil.setOpacity(this._icon, opacity); - + L.DomUtil.setOpacity(this._icon, this.options.opacity); if (this._shadow) { - L.DomUtil.setOpacity(this._shadow, opacity); + L.DomUtil.setOpacity(this._shadow, this.options.opacity); } }, @@ -3805,13 +3809,18 @@ L.DivIcon = L.Icon.extend({ var div = (oldIcon && oldIcon.tagName === 'DIV') ? oldIcon : document.createElement('div'), options = this.options; - div.innerHTML = options.html !== false ? options.html : ''; + if (options.html !== false) { + div.innerHTML = options.html; + } else { + div.innerHTML = ''; + } if (options.bgPos) { - div.style.backgroundPosition = (-options.bgPos.x) + 'px ' + (-options.bgPos.y) + 'px'; + div.style.backgroundPosition = + (-options.bgPos.x) + 'px ' + (-options.bgPos.y) + 'px'; } - this._setIconStyles(div, 'icon'); + this._setIconStyles(div, 'icon'); return div; }, @@ -3833,24 +3842,21 @@ L.Map.mergeOptions({ closePopupOnClick: true }); -L.Popup = L.Layer.extend({ +L.Popup = L.Class.extend({ + includes: L.Mixin.Events, options: { - pane: 'popupPane', - minWidth: 50, maxWidth: 300, - // maxHeight: , - offset: [0, 7], - + // maxHeight: null, autoPan: true, - autoPanPadding: [5, 5], - // autoPanPaddingTopLeft: , - // autoPanPaddingBottomRight: , - closeButton: true, - // keepInView: false, - // className: '', + offset: [0, 7], + autoPanPadding: [5, 5], + // autoPanPaddingTopLeft: null, + // autoPanPaddingBottomRight: null, + keepInView: false, + className: '', zoomAnimation: true }, @@ -3858,51 +3864,70 @@ L.Popup = L.Layer.extend({ L.setOptions(this, options); this._source = source; + this._animated = L.Browser.any3d && this.options.zoomAnimation; + this._isOpen = false; }, onAdd: function (map) { - this._zoomAnimated = this._zoomAnimated && this.options.zoomAnimation; + this._map = map; if (!this._container) { this._initLayout(); } - if (map._fadeAnimated) { + var animFade = map.options.fadeAnimation; + + if (animFade) { L.DomUtil.setOpacity(this._container, 0); } + map._panes.popupPane.appendChild(this._container); + + map.on(this._getEvents(), this); - clearTimeout(this._removeTimeout); - this.getPane().appendChild(this._container); this.update(); - if (map._fadeAnimated) { + if (animFade) { L.DomUtil.setOpacity(this._container, 1); } + this.fire('open'); + map.fire('popupopen', {popup: this}); if (this._source) { - this._source.fire('popupopen', {popup: this}, true); + this._source.fire('popupopen', {popup: this}); } }, + addTo: function (map) { + map.addLayer(this); + return this; + }, + openOn: function (map) { map.openPopup(this); return this; }, onRemove: function (map) { - if (map._fadeAnimated) { + map._panes.popupPane.removeChild(this._container); + + L.Util.falseFn(this._container.offsetWidth); // force reflow + + map.off(this._getEvents(), this); + + if (map.options.fadeAnimation) { L.DomUtil.setOpacity(this._container, 0); - this._removeTimeout = setTimeout(L.bind(L.DomUtil.remove, L.DomUtil, this._container), 200); - } else { - L.DomUtil.remove(this._container); } + this._map = null; + + this.fire('close'); + map.fire('popupclose', {popup: this}); if (this._source) { - this._source.fire('popupclose', {popup: this}, true); + this._source.fire('popupclose', {popup: this}); } }, @@ -3943,25 +3968,23 @@ L.Popup = L.Layer.extend({ this._adjustPan(); }, - getEvents: function () { - var events = {viewreset: this._updatePosition}, - options = this.options; + _getEvents: function () { + var events = { + viewreset: this._updatePosition + }; - if (this._zoomAnimated) { - events.zoomanim = this._animateZoom; + if (this._animated) { + events.zoomanim = this._zoomAnimation; } - if ('closeOnClick' in options ? options.closeOnClick : this._map.options.closePopupOnClick) { + if ('closeOnClick' in this.options ? this.options.closeOnClick : this._map.options.closePopupOnClick) { events.preclick = this._close; } - if (options.keepInView) { + if (this.options.keepInView) { events.moveend = this._adjustPan; } + return events; }, - - isOpen: function () { - return !!this._map && this._map.hasLayer(this); - }, _close: function () { if (this._map) { @@ -3971,25 +3994,29 @@ L.Popup = L.Layer.extend({ _initLayout: function () { var prefix = 'leaflet-popup', - container = this._container = L.DomUtil.create('div', - prefix + ' ' + (this.options.className || '') + - ' leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide')); + containerClass = prefix + ' ' + this.options.className + ' leaflet-zoom-' + + (this._animated ? 'animated' : 'hide'), + container = this._container = L.DomUtil.create('div', containerClass), + closeButton; if (this.options.closeButton) { - var closeButton = this._closeButton = L.DomUtil.create('a', prefix + '-close-button', container); + closeButton = this._closeButton = + L.DomUtil.create('a', prefix + '-close-button', container); closeButton.href = '#close'; closeButton.innerHTML = '×'; + L.DomEvent.disableClickPropagation(closeButton); L.DomEvent.on(closeButton, 'click', this._onCloseButtonClick, this); } - var wrapper = this._wrapper = L.DomUtil.create('div', prefix + '-content-wrapper', container); + var wrapper = this._wrapper = + L.DomUtil.create('div', prefix + '-content-wrapper', container); + L.DomEvent.disableClickPropagation(wrapper); + this._contentNode = L.DomUtil.create('div', prefix + '-content', wrapper); - L.DomEvent - .disableClickPropagation(wrapper) - .disableScrollPropagation(this._contentNode) - .on(wrapper, 'contextmenu', L.DomEvent.stopPropagation); + L.DomEvent.disableScrollPropagation(this._contentNode); + L.DomEvent.on(wrapper, 'contextmenu', L.DomEvent.stopPropagation); this._tipContainer = L.DomUtil.create('div', prefix + '-tip-container', container); this._tip = L.DomUtil.create('div', prefix + '-tip', this._tipContainer); @@ -3998,15 +4025,13 @@ L.Popup = L.Layer.extend({ _updateContent: function () { if (!this._content) { return; } - var node = this._contentNode; - if (typeof this._content === 'string') { - node.innerHTML = this._content; + this._contentNode.innerHTML = this._content; } else { - while (node.hasChildNodes()) { - node.removeChild(node.firstChild); + while (this._contentNode.hasChildNodes()) { + this._contentNode.removeChild(this._contentNode.firstChild); } - node.appendChild(this._content); + this._contentNode.appendChild(this._content); } this.fire('contentupdate'); }, @@ -4045,24 +4070,24 @@ L.Popup = L.Layer.extend({ if (!this._map) { return; } var pos = this._map.latLngToLayerPoint(this._latlng), + animated = this._animated, offset = L.point(this.options.offset); - if (this._zoomAnimated) { + if (animated) { L.DomUtil.setPosition(this._container, pos); - } else { - offset = offset.add(pos); } - var bottom = this._containerBottom = -offset.y, - left = this._containerLeft = -Math.round(this._containerWidth / 2) + offset.x; + this._containerBottom = -offset.y - (animated ? 0 : pos.y); + this._containerLeft = -Math.round(this._containerWidth / 2) + offset.x + (animated ? 0 : pos.x); // bottom position the popup in case the height of the popup changes (images loading etc) - this._container.style.bottom = bottom + 'px'; - this._container.style.left = left + 'px'; + this._container.style.bottom = this._containerBottom + 'px'; + this._container.style.left = this._containerLeft + 'px'; }, - _animateZoom: function (e) { - var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center); + _zoomAnimation: function (opt) { + var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center); + L.DomUtil.setPosition(this._container, pos); }, @@ -4072,9 +4097,10 @@ L.Popup = L.Layer.extend({ var map = this._map, containerHeight = this._container.offsetHeight, containerWidth = this._containerWidth, + layerPos = new L.Point(this._containerLeft, -containerHeight - this._containerBottom); - if (this._zoomAnimated) { + if (this._animated) { layerPos._add(L.DomUtil.getPosition(this._container)); } @@ -4119,21 +4145,17 @@ L.popup = function (options, source) { L.Map.include({ openPopup: function (popup, latlng, options) { // (Popup) or (String || HTMLElement, LatLng[, Object]) + this.closePopup(); + if (!(popup instanceof L.Popup)) { var content = popup; - popup = new L.Popup(options).setContent(content); - } - - if (latlng) { - popup.setLatLng(latlng); - } - - if (this.hasLayer(popup)) { - return this; + popup = new L.Popup(options) + .setLatLng(latlng) + .setContent(content); } + popup._isOpen = true; - this.closePopup(); this._popup = popup; return this.addLayer(popup); }, @@ -4145,117 +4167,102 @@ L.Map.include({ } if (popup) { this.removeLayer(popup); + popup._isOpen = false; } return this; } }); -/* - * Adds popup-related methods to all layers. - */ - -L.Layer.include({ - - bindPopup: function (content, options) { - - if (content instanceof L.Popup) { - this._popup = content; - content._source = this; - } else { - if (!this._popup || options) { - this._popup = new L.Popup(options, this); - } - this._popup.setContent(content); - } - - if (!this._popupHandlersAdded) { - this.on({ - click: this._openPopup, - remove: this.closePopup, - move: this._movePopup - }); - this._popupHandlersAdded = true; - } - - return this; - }, - - unbindPopup: function () { - if (this._popup) { - this.on({ - click: this._openPopup, - remove: this.closePopup, - move: this._movePopup - }); - this._popupHandlersAdded = false; - this._popup = null; - } - return this; - }, - - openPopup: function (latlng) { - if (this._popup && this._map) { - this._map.openPopup(this._popup, latlng || this._latlng || this.getCenter()); - } - return this; - }, - - closePopup: function () { - if (this._popup) { - this._popup._close(); - } - return this; - }, - - togglePopup: function () { - if (this._popup) { - if (this._popup._map) { - this.closePopup(); - } else { - this.openPopup(); - } - } - return this; - }, - - setPopupContent: function (content) { - if (this._popup) { - this._popup.setContent(content); - } - return this; - }, - - getPopup: function () { - return this._popup; - }, - - _openPopup: function (e) { - this._map.openPopup(this._popup, e.latlng); - }, - - _movePopup: function (e) { - this._popup.setLatLng(e.latlng); - } -}); - - /* * Popup extension to L.Marker, adding popup-related methods. */ L.Marker.include({ - bindPopup: function (content, options) { - var anchor = L.point(this.options.icon.options.popupAnchor || [0, 0]) - .add(L.Popup.prototype.options.offset); + openPopup: function () { + if (this._popup && this._map && !this._map.hasLayer(this._popup)) { + this._popup.setLatLng(this._latlng); + this._map.openPopup(this._popup); + } - options = L.extend({offset: anchor}, options); + return this; + }, - return L.Layer.prototype.bindPopup.call(this, content, options); + closePopup: function () { + if (this._popup) { + this._popup._close(); + } + return this; }, - _openPopup: L.Layer.prototype.togglePopup -}); + togglePopup: function () { + if (this._popup) { + if (this._popup._isOpen) { + this.closePopup(); + } else { + this.openPopup(); + } + } + return this; + }, + + bindPopup: function (content, options) { + var anchor = L.point(this.options.icon.options.popupAnchor || [0, 0]); + + anchor = anchor.add(L.Popup.prototype.options.offset); + + if (options && options.offset) { + anchor = anchor.add(options.offset); + } + + options = L.extend({offset: anchor}, options); + + if (!this._popupHandlersAdded) { + this + .on('click', this.togglePopup, this) + .on('remove', this.closePopup, this) + .on('move', this._movePopup, this); + this._popupHandlersAdded = true; + } + + if (content instanceof L.Popup) { + L.setOptions(content, options); + this._popup = content; + } else { + this._popup = new L.Popup(options, this) + .setContent(content); + } + + return this; + }, + + setPopupContent: function (content) { + if (this._popup) { + this._popup.setContent(content); + } + return this; + }, + + unbindPopup: function () { + if (this._popup) { + this._popup = null; + this + .off('click', this.togglePopup, this) + .off('remove', this.closePopup, this) + .off('move', this._movePopup, this); + this._popupHandlersAdded = false; + } + return this; + }, + + getPopup: function () { + return this._popup; + }, + + _movePopup: function (e) { + this._popup.setLatLng(e.latlng); + } +}); /* @@ -4263,8 +4270,7 @@ L.Marker.include({ * you can manipulate the group (e.g. add/remove it) as one layer. */ -L.LayerGroup = L.Layer.extend({ - +L.LayerGroup = L.Class.extend({ initialize: function (layers) { this._layers = {}; @@ -4302,13 +4308,13 @@ L.LayerGroup = L.Layer.extend({ }, hasLayer: function (layer) { - return !!layer && (layer in this._layers || this.getLayerId(layer) in this._layers); + if (!layer) { return false; } + + return (layer in this._layers || this.getLayerId(layer) in this._layers); }, clearLayers: function () { - for (var i in this._layers) { - this.removeLayer(this._layers[i]); - } + this.eachLayer(this.removeLayer, this); return this; }, @@ -4328,15 +4334,18 @@ L.LayerGroup = L.Layer.extend({ }, onAdd: function (map) { - for (var i in this._layers) { - map.addLayer(this._layers[i]); - } + this._map = map; + this.eachLayer(map.addLayer, map); }, onRemove: function (map) { - for (var i in this._layers) { - map.removeLayer(this._layers[i]); - } + this.eachLayer(map.removeLayer, map); + this._map = null; + }, + + addTo: function (map) { + map.addLayer(this); + return this; }, eachLayer: function (method, context) { @@ -4379,13 +4388,20 @@ L.layerGroup = function (layers) { */ L.FeatureGroup = L.LayerGroup.extend({ + includes: L.Mixin.Events, + + statics: { + EVENTS: 'click dblclick mouseover mouseout mousemove contextmenu popupopen popupclose' + }, addLayer: function (layer) { if (this.hasLayer(layer)) { return this; } - layer.addEventParent(this); + if ('on' in layer) { + layer.on(L.FeatureGroup.EVENTS, this._propagateEvent, this); + } L.LayerGroup.prototype.addLayer.call(this, layer); @@ -4404,7 +4420,7 @@ L.FeatureGroup = L.LayerGroup.extend({ layer = this._layers[layer]; } - layer.removeEventParent(this); + layer.off(L.FeatureGroup.EVENTS, this._propagateEvent, this); L.LayerGroup.prototype.removeLayer.call(this, layer); @@ -4446,10 +4462,18 @@ L.FeatureGroup = L.LayerGroup.extend({ var bounds = new L.LatLngBounds(); this.eachLayer(function (layer) { - bounds.extend(layer.getBounds ? layer.getBounds() : layer.getLatLng()); + bounds.extend(layer instanceof L.Marker ? layer.getLatLng() : layer.getBounds()); }); return bounds; + }, + + _propagateEvent: function (e) { + e = L.extend({ + layer: e.target, + target: this + }, e); + this.fire(e.type, e); } }); @@ -4458,1546 +4482,1619 @@ L.featureGroup = function (layers) { }; -/* - * L.Renderer is a base class for renderer implementations (SVG, Canvas); - * handles renderer container, bounds and zoom animation. - */ - -L.Renderer = L.Layer.extend({ - - options: { - // how much to extend the clip area around the map view (relative to its size) - // e.g. 0.1 would be 10% of map view in each direction; defaults to clip with the map view - padding: 0 - }, - - initialize: function (options) { - L.setOptions(this, options); - L.stamp(this); - }, - - onAdd: function () { - if (!this._container) { - this._initContainer(); // defined by renderer implementations - - if (this._zoomAnimated) { - L.DomUtil.addClass(this._container, 'leaflet-zoom-animated'); - } - } - - this.getPane().appendChild(this._container); - this._update(); - }, - - onRemove: function () { - L.DomUtil.remove(this._container); - }, - - getEvents: function () { - var events = { - moveend: this._update - }; - if (this._zoomAnimated) { - events.zoomanim = this._animateZoom; - } - return events; - }, - - _animateZoom: function (e) { - var origin = e.origin.subtract(this._map._getCenterLayerPoint()), - offset = this._bounds.min.add(origin.multiplyBy(1 - e.scale)); - - L.DomUtil.setTransform(this._container, offset, e.scale); - }, - - _update: function () { - // update pixel bounds of renderer container (for positioning/sizing/clipping later) - var p = this.options.padding, - size = this._map.getSize(), - min = this._map.containerPointToLayerPoint(size.multiplyBy(-p)).round(); - - this._bounds = new L.Bounds(min, min.add(size.multiplyBy(1 + p * 2)).round()); - } -}); - - -L.Map.include({ - // used by each vector layer to decide which renderer to use - getRenderer: function (layer) { - var renderer = layer.options.renderer || this.options.renderer || this._renderer; - - if (!renderer) { - renderer = this._renderer = (L.SVG && L.svg()) || (L.Canvas && L.canvas()); - } - - if (!this.hasLayer(renderer)) { - this.addLayer(renderer); - } - return renderer; - } -}); - - -/* - * L.Path is the base class for all Leaflet vector layers like polygons and circles. - */ - -L.Path = L.Layer.extend({ - - options: { - stroke: true, - color: '#3388ff', - weight: 3, - opacity: 1, - lineCap: 'round', - lineJoin: 'round', - // dashArray: null - // dashOffset: null - - // fill: false - // fillColor: same as color by default - fillOpacity: 0.2, - - // className: '' - interactive: true - }, - - onAdd: function () { - this._renderer = this._map.getRenderer(this); - this._renderer._initPath(this); - - // defined in children classes - this._project(); - this._update(); - - this._renderer._addPath(this); - }, - - onRemove: function () { - this._renderer._removePath(this); - }, - - getEvents: function () { - return { - viewreset: this._project, - moveend: this._update - }; - }, - - redraw: function () { - if (this._map) { - this._renderer._updatePath(this); - } - return this; - }, - - setStyle: function (style) { - L.setOptions(this, style); - if (this._renderer) { - this._renderer._updateStyle(this); - } - return this; - }, - - bringToFront: function () { - this._renderer._bringToFront(this); - return this; - }, - - bringToBack: function () { - this._renderer._bringToBack(this); - return this; - }, - - _fireMouseEvent: function (e, type) { - this._map._fireMouseEvent(this, e, type, true); - }, - - _clickTolerance: function () { - // used when doing hit detection for Canvas layers - return (this.options.stroke ? this.options.weight / 2 : 0) + (L.Browser.touch ? 10 : 0); - } -}); - - /* - * L.LineUtil contains different utility functions for line segments - * and polylines (clipping, simplification, distances, etc.) + * L.Path is a base class for rendering vector paths on a map. Inherited by Polyline, Circle, etc. */ -/*jshint bitwise:false */ // allow bitwise operations for this file +L.Path = L.Class.extend({ + includes: [L.Mixin.Events], -L.LineUtil = { + statics: { + // how much to extend the clip area around the map view + // (relative to its size, e.g. 0.5 is half the screen in each direction) + // set it so that SVG element doesn't exceed 1280px (vectors flicker on dragend if it is) + CLIP_PADDING: (function () { + var max = L.Browser.mobile ? 1280 : 2000, + target = (max / Math.max(window.outerWidth, window.outerHeight) - 1) / 2; + return Math.max(0, Math.min(0.5, target)); + })() + }, - // Simplify polyline with vertex reduction and Douglas-Peucker simplification. - // Improves rendering performance dramatically by lessening the number of points to draw. + options: { + stroke: true, + color: '#0033ff', + dashArray: null, + lineCap: null, + lineJoin: null, + weight: 5, + opacity: 0.5, - simplify: function (/*Point[]*/ points, /*Number*/ tolerance) { - if (!tolerance || !points.length) { - return points.slice(); + fill: false, + fillColor: null, //same as color by default + fillOpacity: 0.2, + + clickable: true + }, + + initialize: function (options) { + L.setOptions(this, options); + }, + + onAdd: function (map) { + this._map = map; + + if (!this._container) { + this._initElements(); + this._initEvents(); } - var sqTolerance = tolerance * tolerance; + this.projectLatlngs(); + this._updatePath(); - // stage 1: vertex reduction - points = this._reducePoints(points, sqTolerance); + if (this._container) { + this._map._pathRoot.appendChild(this._container); + } - // stage 2: Douglas-Peucker simplification - points = this._simplifyDP(points, sqTolerance); + this.fire('add'); - return points; + map.on({ + 'viewreset': this.projectLatlngs, + 'moveend': this._updatePath + }, this); }, - // distance from a point to a segment between two points - pointToSegmentDistance: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2) { - return Math.sqrt(this._sqClosestPointOnSegment(p, p1, p2, true)); + addTo: function (map) { + map.addLayer(this); + return this; }, - closestPointOnSegment: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2) { - return this._sqClosestPointOnSegment(p, p1, p2); - }, + onRemove: function (map) { + map._pathRoot.removeChild(this._container); - // Douglas-Peucker simplification, see http://en.wikipedia.org/wiki/Douglas-Peucker_algorithm - _simplifyDP: function (points, sqTolerance) { + // Need to fire remove event before we set _map to null as the event hooks might need the object + this.fire('remove'); + this._map = null; - var len = points.length, - ArrayConstructor = typeof Uint8Array !== undefined + '' ? Uint8Array : Array, - markers = new ArrayConstructor(len); + if (L.Browser.vml) { + this._container = null; + this._stroke = null; + this._fill = null; + } - markers[0] = markers[len - 1] = 1; + map.off({ + 'viewreset': this.projectLatlngs, + 'moveend': this._updatePath + }, this); + }, - this._simplifyDPStep(points, markers, sqTolerance, 0, len - 1); + projectLatlngs: function () { + // do all projection stuff here + }, - var i, - newPoints = []; + setStyle: function (style) { + L.setOptions(this, style); - for (i = 0; i < len; i++) { - if (markers[i]) { - newPoints.push(points[i]); - } + if (this._container) { + this._updateStyle(); } - return newPoints; + return this; }, - _simplifyDPStep: function (points, markers, sqTolerance, first, last) { + redraw: function () { + if (this._map) { + this.projectLatlngs(); + this._updatePath(); + } + return this; + } +}); - var maxSqDist = 0, - index, i, sqDist; +L.Map.include({ + _updatePathViewport: function () { + var p = L.Path.CLIP_PADDING, + size = this.getSize(), + panePos = L.DomUtil.getPosition(this._mapPane), + min = panePos.multiplyBy(-1)._subtract(size.multiplyBy(p)._round()), + max = min.add(size.multiplyBy(1 + p * 2)._round()); - for (i = first + 1; i <= last - 1; i++) { - sqDist = this._sqClosestPointOnSegment(points[i], points[first], points[last], true); + this._pathViewport = new L.Bounds(min, max); + } +}); + + +/* + * Extends L.Path with SVG-specific rendering code. + */ - if (sqDist > maxSqDist) { - index = i; - maxSqDist = sqDist; - } - } +L.Path.SVG_NS = 'http://www.w3.org/2000/svg'; - if (maxSqDist > sqTolerance) { - markers[index] = 1; +L.Browser.svg = !!(document.createElementNS && document.createElementNS(L.Path.SVG_NS, 'svg').createSVGRect); - this._simplifyDPStep(points, markers, sqTolerance, first, index); - this._simplifyDPStep(points, markers, sqTolerance, index, last); - } +L.Path = L.Path.extend({ + statics: { + SVG: L.Browser.svg }, - // reduce points that are too close to each other to a single point - _reducePoints: function (points, sqTolerance) { - var reducedPoints = [points[0]]; + bringToFront: function () { + var root = this._map._pathRoot, + path = this._container; - for (var i = 1, prev = 0, len = points.length; i < len; i++) { - if (this._sqDist(points[i], points[prev]) > sqTolerance) { - reducedPoints.push(points[i]); - prev = i; - } - } - if (prev < len - 1) { - reducedPoints.push(points[len - 1]); + if (path && root.lastChild !== path) { + root.appendChild(path); } - return reducedPoints; + return this; }, - // Cohen-Sutherland line clipping algorithm. - // Used to avoid rendering parts of a polyline that are not currently visible. + bringToBack: function () { + var root = this._map._pathRoot, + path = this._container, + first = root.firstChild; - clipSegment: function (a, b, bounds, useLastCode) { - var codeA = useLastCode ? this._lastCode : this._getBitCode(a, bounds), - codeB = this._getBitCode(b, bounds), + if (path && first !== path) { + root.insertBefore(path, first); + } + return this; + }, - codeOut, p, newCode; + getPathString: function () { + // form path string here + }, - // save 2nd code to avoid calculating it on the next segment - this._lastCode = codeB; + _createElement: function (name) { + return document.createElementNS(L.Path.SVG_NS, name); + }, - while (true) { - // if a,b is inside the clip window (trivial accept) - if (!(codeA | codeB)) { - return [a, b]; - // if a,b is outside the clip window (trivial reject) - } else if (codeA & codeB) { - return false; - // other cases - } else { - codeOut = codeA || codeB; - p = this._getEdgeIntersection(a, b, codeOut, bounds); - newCode = this._getBitCode(p, bounds); + _initElements: function () { + this._map._initPathRoot(); + this._initPath(); + this._initStyle(); + }, - if (codeOut === codeA) { - a = p; - codeA = newCode; - } else { - b = p; - codeB = newCode; - } - } + _initPath: function () { + this._container = this._createElement('g'); + + this._path = this._createElement('path'); + + if (this.options.className) { + L.DomUtil.addClass(this._path, this.options.className); } + + this._container.appendChild(this._path); }, - _getEdgeIntersection: function (a, b, code, bounds) { - var dx = b.x - a.x, - dy = b.y - a.y, - min = bounds.min, - max = bounds.max, - x, y; + _initStyle: function () { + if (this.options.stroke) { + this._path.setAttribute('stroke-linejoin', 'round'); + this._path.setAttribute('stroke-linecap', 'round'); + } + if (this.options.fill) { + this._path.setAttribute('fill-rule', 'evenodd'); + } + if (this.options.pointerEvents) { + this._path.setAttribute('pointer-events', this.options.pointerEvents); + } + if (!this.options.clickable && !this.options.pointerEvents) { + this._path.setAttribute('pointer-events', 'none'); + } + this._updateStyle(); + }, - if (code & 8) { // top - x = a.x + dx * (max.y - a.y) / dy; - y = max.y; + _updateStyle: function () { + if (this.options.stroke) { + this._path.setAttribute('stroke', this.options.color); + this._path.setAttribute('stroke-opacity', this.options.opacity); + this._path.setAttribute('stroke-width', this.options.weight); + if (this.options.dashArray) { + this._path.setAttribute('stroke-dasharray', this.options.dashArray); + } else { + this._path.removeAttribute('stroke-dasharray'); + } + if (this.options.lineCap) { + this._path.setAttribute('stroke-linecap', this.options.lineCap); + } + if (this.options.lineJoin) { + this._path.setAttribute('stroke-linejoin', this.options.lineJoin); + } + } else { + this._path.setAttribute('stroke', 'none'); + } + if (this.options.fill) { + this._path.setAttribute('fill', this.options.fillColor || this.options.color); + this._path.setAttribute('fill-opacity', this.options.fillOpacity); + } else { + this._path.setAttribute('fill', 'none'); + } + }, - } else if (code & 4) { // bottom - x = a.x + dx * (min.y - a.y) / dy; - y = min.y; + _updatePath: function () { + var str = this.getPathString(); + if (!str) { + // fix webkit empty string parsing bug + str = 'M0 0'; + } + this._path.setAttribute('d', str); + }, - } else if (code & 2) { // right - x = max.x; - y = a.y + dy * (max.x - a.x) / dx; + // TODO remove duplication with L.Map + _initEvents: function () { + if (this.options.clickable) { + if (L.Browser.svg || !L.Browser.vml) { + L.DomUtil.addClass(this._path, 'leaflet-clickable'); + } - } else if (code & 1) { // left - x = min.x; - y = a.y + dy * (min.x - a.x) / dx; + L.DomEvent.on(this._container, 'click', this._onMouseClick, this); + + var events = ['dblclick', 'mousedown', 'mouseover', + 'mouseout', 'mousemove', 'contextmenu']; + for (var i = 0; i < events.length; i++) { + L.DomEvent.on(this._container, events[i], this._fireMouseEvent, this); + } } + }, - return new L.Point(x, y, true); + _onMouseClick: function (e) { + if (this._map.dragging && this._map.dragging.moved()) { return; } + + this._fireMouseEvent(e); }, - _getBitCode: function (/*Point*/ p, bounds) { - var code = 0; + _fireMouseEvent: function (e) { + if (!this.hasEventListeners(e.type)) { return; } - if (p.x < bounds.min.x) { // left - code |= 1; - } else if (p.x > bounds.max.x) { // right - code |= 2; + var map = this._map, + containerPoint = map.mouseEventToContainerPoint(e), + layerPoint = map.containerPointToLayerPoint(containerPoint), + latlng = map.layerPointToLatLng(layerPoint); + + this.fire(e.type, { + latlng: latlng, + layerPoint: layerPoint, + containerPoint: containerPoint, + originalEvent: e + }); + + if (e.type === 'contextmenu') { + L.DomEvent.preventDefault(e); + } + if (e.type !== 'mousemove') { + L.DomEvent.stopPropagation(e); } + } +}); - if (p.y < bounds.min.y) { // bottom - code |= 4; - } else if (p.y > bounds.max.y) { // top - code |= 8; +L.Map.include({ + _initPathRoot: function () { + if (!this._pathRoot) { + this._pathRoot = L.Path.prototype._createElement('svg'); + this._panes.overlayPane.appendChild(this._pathRoot); + + if (this.options.zoomAnimation && L.Browser.any3d) { + L.DomUtil.addClass(this._pathRoot, 'leaflet-zoom-animated'); + + this.on({ + 'zoomanim': this._animatePathZoom, + 'zoomend': this._endPathZoom + }); + } else { + L.DomUtil.addClass(this._pathRoot, 'leaflet-zoom-hide'); + } + + this.on('moveend', this._updateSvgViewport); + this._updateSvgViewport(); } + }, - return code; + _animatePathZoom: function (e) { + var scale = this.getZoomScale(e.zoom), + offset = this._getCenterOffset(e.center)._multiplyBy(-scale)._add(this._pathViewport.min); + + this._pathRoot.style[L.DomUtil.TRANSFORM] = + L.DomUtil.getTranslateString(offset) + ' scale(' + scale + ') '; + + this._pathZooming = true; }, - // square distance (to avoid unnecessary Math.sqrt calls) - _sqDist: function (p1, p2) { - var dx = p2.x - p1.x, - dy = p2.y - p1.y; - return dx * dx + dy * dy; + _endPathZoom: function () { + this._pathZooming = false; }, - // return closest point on segment or distance to that point - _sqClosestPointOnSegment: function (p, p1, p2, sqDist) { - var x = p1.x, - y = p1.y, - dx = p2.x - x, - dy = p2.y - y, - dot = dx * dx + dy * dy, - t; + _updateSvgViewport: function () { - if (dot > 0) { - t = ((p.x - x) * dx + (p.y - y) * dy) / dot; + if (this._pathZooming) { + // Do not update SVGs while a zoom animation is going on otherwise the animation will break. + // When the zoom animation ends we will be updated again anyway + // This fixes the case where you do a momentum move and zoom while the move is still ongoing. + return; + } - if (t > 1) { - x = p2.x; - y = p2.y; - } else if (t > 0) { - x += dx * t; - y += dy * t; - } + this._updatePathViewport(); + + var vp = this._pathViewport, + min = vp.min, + max = vp.max, + width = max.x - min.x, + height = max.y - min.y, + root = this._pathRoot, + pane = this._panes.overlayPane; + + // Hack to make flicker on drag end on mobile webkit less irritating + if (L.Browser.mobileWebkit) { + pane.removeChild(root); } - dx = p.x - x; - dy = p.y - y; + L.DomUtil.setPosition(root, min); + root.setAttribute('width', width); + root.setAttribute('height', height); + root.setAttribute('viewBox', [min.x, min.y, width, height].join(' ')); - return sqDist ? dx * dx + dy * dy : new L.Point(x, y); + if (L.Browser.mobileWebkit) { + pane.appendChild(root); + } } -}; - - -/* - * L.Polyline implements polyline vector layer (a set of points connected with lines) - */ - -L.Polyline = L.Path.extend({ - - options: { - // how much to simplify the polyline on each zoom level - // more = better performance and smoother look, less = more accurate - smoothFactor: 1.0 - // noClip: false - }, - - initialize: function (latlngs, options) { - L.setOptions(this, options); - this._setLatLngs(latlngs); - }, - - getLatLngs: function () { - // TODO rings - return this._latlngs; - }, - - setLatLngs: function (latlngs) { - this._setLatLngs(latlngs); - return this.redraw(); - }, - - addLatLng: function (latlng) { - // TODO rings - latlng = L.latLng(latlng); - this._latlngs.push(latlng); - this._bounds.extend(latlng); - return this.redraw(); - }, - - spliceLatLngs: function () { - // TODO rings - var removed = [].splice.apply(this._latlngs, arguments); - this._setLatLngs(this._latlngs); - this.redraw(); - return removed; - }, - - closestLayerPoint: function (p) { - var minDistance = Infinity, - minPoint = null, - closest = L.LineUtil._sqClosestPointOnSegment, - p1, p2; - - for (var j = 0, jLen = this._parts.length; j < jLen; j++) { - var points = this._parts[j]; - - for (var i = 1, len = points.length; i < len; i++) { - p1 = points[i - 1]; - p2 = points[i]; - - var sqDist = closest(p, p1, p2, true); - - if (sqDist < minDistance) { - minDistance = sqDist; - minPoint = closest(p, p1, p2); - } - } - } - if (minPoint) { - minPoint.distance = Math.sqrt(minDistance); - } - return minPoint; - }, - - getCenter: function () { - var i, halfDist, segDist, dist, p1, p2, ratio, - points = this._rings[0], - len = points.length; - - // polyline centroid algorithm; only uses the first ring if there are multiple - - for (i = 0, halfDist = 0; i < len - 1; i++) { - halfDist += points[i].distanceTo(points[i + 1]) / 2; - } - - for (i = 0, dist = 0; i < len - 1; i++) { - p1 = points[i]; - p2 = points[i + 1]; - segDist = p1.distanceTo(p2); - dist += segDist; - - if (dist > halfDist) { - ratio = (dist - halfDist) / segDist; - return this._map.layerPointToLatLng([ - p2.x - ratio * (p2.x - p1.x), - p2.y - ratio * (p2.y - p1.y) - ]); - } - } - }, - - getBounds: function () { - return this._bounds; - }, - - _setLatLngs: function (latlngs) { - this._bounds = new L.LatLngBounds(); - this._latlngs = this._convertLatLngs(latlngs); - }, - - // recursively convert latlngs input into actual LatLng instances; calculate bounds along the way - _convertLatLngs: function (latlngs) { - var result = [], - flat = this._flat(latlngs); - - for (var i = 0, len = latlngs.length; i < len; i++) { - if (flat) { - result[i] = L.latLng(latlngs[i]); - this._bounds.extend(result[i]); - } else { - result[i] = this._convertLatLngs(latlngs[i]); - } - } - - return result; - }, - - _flat: function (latlngs) { - // true if it's a flat array of latlngs; false if nested - return !L.Util.isArray(latlngs[0]) || typeof latlngs[0][0] !== 'object'; - }, - - _project: function () { - this._rings = []; - this._projectLatlngs(this._latlngs, this._rings); - - // project bounds as well to use later for Canvas hit detection/etc. - var w = this._clickTolerance(), - p = new L.Point(w, -w); - - if (this._latlngs.length) { - this._pxBounds = new L.Bounds( - this._map.latLngToLayerPoint(this._bounds.getSouthWest())._subtract(p), - this._map.latLngToLayerPoint(this._bounds.getNorthEast())._add(p)); - } - }, - - // recursively turns latlngs into a set of rings with projected coordinates - _projectLatlngs: function (latlngs, result) { - - var flat = latlngs[0] instanceof L.LatLng, - len = latlngs.length, - i, ring; - - if (flat) { - ring = []; - for (i = 0; i < len; i++) { - ring[i] = this._map.latLngToLayerPoint(latlngs[i]); - } - result.push(ring); - } else { - for (i = 0; i < len; i++) { - this._projectLatlngs(latlngs[i], result); - } - } - }, - - // clip polyline by renderer bounds so that we have less to render for performance - _clipPoints: function () { - if (this.options.noClip) { - this._parts = this._rings; - return; - } - - this._parts = []; - - var parts = this._parts, - bounds = this._renderer._bounds, - i, j, k, len, len2, segment, points; - - for (i = 0, k = 0, len = this._rings.length; i < len; i++) { - points = this._rings[i]; - - for (j = 0, len2 = points.length; j < len2 - 1; j++) { - segment = L.LineUtil.clipSegment(points[j], points[j + 1], bounds, j); - - if (!segment) { continue; } - - parts[k] = parts[k] || []; - parts[k].push(segment[0]); - - // if segment goes out of screen, or it's the last one, it's the end of the line part - if ((segment[1] !== points[j + 1]) || (j === len2 - 2)) { - parts[k].push(segment[1]); - k++; - } - } - } - }, - - // simplify each clipped part of the polyline for performance - _simplifyPoints: function () { - var parts = this._parts, - tolerance = this.options.smoothFactor; - - for (var i = 0, len = parts.length; i < len; i++) { - parts[i] = L.LineUtil.simplify(parts[i], tolerance); - } - }, - - _update: function () { - if (!this._map) { return; } - - this._clipPoints(); - this._simplifyPoints(); - this._updatePath(); - }, - - _updatePath: function () { - this._renderer._updatePoly(this); - } -}); - -L.polyline = function (latlngs, options) { - return new L.Polyline(latlngs, options); -}; +}); /* - * L.PolyUtil contains utility functions for polygons (clipping, etc.). + * Popup extension to L.Path (polylines, polygons, circles), adding popup-related methods. */ -/*jshint bitwise:false */ // allow bitwise operations here +L.Path.include({ -L.PolyUtil = {}; + bindPopup: function (content, options) { -/* - * Sutherland-Hodgeman polygon clipping algorithm. - * Used to avoid rendering parts of a polygon that are not currently visible. - */ -L.PolyUtil.clipPolygon = function (points, bounds) { - var clippedPoints, - edges = [1, 4, 2, 8], - i, j, k, - a, b, - len, edge, p, - lu = L.LineUtil; + if (content instanceof L.Popup) { + this._popup = content; + } else { + if (!this._popup || options) { + this._popup = new L.Popup(options, this); + } + this._popup.setContent(content); + } - for (i = 0, len = points.length; i < len; i++) { - points[i]._code = lu._getBitCode(points[i], bounds); - } + if (!this._popupHandlersAdded) { + this + .on('click', this._openPopup, this) + .on('remove', this.closePopup, this); - // for each edge (left, bottom, right, top) - for (k = 0; k < 4; k++) { - edge = edges[k]; - clippedPoints = []; + this._popupHandlersAdded = true; + } - for (i = 0, len = points.length, j = len - 1; i < len; j = i++) { - a = points[i]; - b = points[j]; + return this; + }, - // if a is inside the clip window - if (!(a._code & edge)) { - // if b is outside the clip window (a->b goes out of screen) - if (b._code & edge) { - p = lu._getEdgeIntersection(b, a, edge, bounds); - p._code = lu._getBitCode(p, bounds); - clippedPoints.push(p); - } - clippedPoints.push(a); + unbindPopup: function () { + if (this._popup) { + this._popup = null; + this + .off('click', this._openPopup) + .off('remove', this.closePopup); - // else if b is inside the clip window (a->b enters the screen) - } else if (!(b._code & edge)) { - p = lu._getEdgeIntersection(b, a, edge, bounds); - p._code = lu._getBitCode(p, bounds); - clippedPoints.push(p); - } + this._popupHandlersAdded = false; } - points = clippedPoints; - } + return this; + }, - return points; -}; - - -/* - * L.Polygon implements polygon vector layer (closed polyline with a fill inside). - */ - -L.Polygon = L.Polyline.extend({ - - options: { - fill: true - }, - - getCenter: function () { - var i, j, len, p1, p2, f, area, x, y, - points = this._rings[0]; - - // polygon centroid algorithm; only uses the first ring if there are multiple - - area = x = y = 0; - - for (i = 0, len = points.length, j = len - 1; i < len; j = i++) { - p1 = points[i]; - p2 = points[j]; - - f = p1.y * p2.x - p2.y * p1.x; - x += (p1.x + p2.x) * f; - y += (p1.y + p2.y) * f; - area += f * 3; - } - - return this._map.layerPointToLatLng([x / area, y / area]); - }, - - _convertLatLngs: function (latlngs) { - var result = L.Polyline.prototype._convertLatLngs.call(this, latlngs), - len = result.length; - - // remove last point if it equals first one - if (len >= 2 && result[0] instanceof L.LatLng && result[0].equals(result[len - 1])) { - result.pop(); - } - return result; - }, - - _clipPoints: function () { - if (this.options.noClip) { - this._parts = this._rings; - return; - } - - // polygons need a different clipping algorithm so we redefine that - - var bounds = this._renderer._bounds, - w = this.options.weight, - p = new L.Point(w, w); - - // increase clip padding by stroke width to avoid stroke on clip edges - bounds = new L.Bounds(bounds.min.subtract(p), bounds.max.add(p)); - - this._parts = []; - - for (var i = 0, len = this._rings.length, clipped; i < len; i++) { - clipped = L.PolyUtil.clipPolygon(this._rings[i], bounds); - if (clipped.length) { - this._parts.push(clipped); - } - } - }, - - _updatePath: function () { - this._renderer._updatePoly(this, true); - } -}); - -L.polygon = function (latlngs, options) { - return new L.Polygon(latlngs, options); -}; - - -/* - * L.Rectangle extends Polygon and creates a rectangle when passed a LatLngBounds object. - */ - -L.Rectangle = L.Polygon.extend({ - initialize: function (latLngBounds, options) { - L.Polygon.prototype.initialize.call(this, this._boundsToLatLngs(latLngBounds), options); - }, - - setBounds: function (latLngBounds) { - this.setLatLngs(this._boundsToLatLngs(latLngBounds)); - }, - - _boundsToLatLngs: function (latLngBounds) { - latLngBounds = L.latLngBounds(latLngBounds); - return [ - latLngBounds.getSouthWest(), - latLngBounds.getNorthWest(), - latLngBounds.getNorthEast(), - latLngBounds.getSouthEast() - ]; - } -}); - -L.rectangle = function (latLngBounds, options) { - return new L.Rectangle(latLngBounds, options); -}; - - -/* - * L.CircleMarker is a circle overlay with a permanent pixel radius. - */ - -L.CircleMarker = L.Path.extend({ - - options: { - fill: true, - radius: 10 - }, - - initialize: function (latlng, options) { - L.setOptions(this, options); - this._latlng = L.latLng(latlng); - this._radius = this.options.radius; - }, - - setLatLng: function (latlng) { - this._latlng = L.latLng(latlng); - this.redraw(); - return this.fire('move', {latlng: this._latlng}); - }, - - getLatLng: function () { - return this._latlng; - }, - - setRadius: function (radius) { - this.options.radius = this._radius = radius; - return this.redraw(); - }, - - getRadius: function () { - return this._radius; - }, - - setStyle : function (options) { - var radius = options && options.radius || this._radius; - L.Path.prototype.setStyle.call(this, options); - this.setRadius(radius); - return this; - }, - - _project: function () { - this._point = this._map.latLngToLayerPoint(this._latlng); - this._updateBounds(); - }, - - _updateBounds: function () { - var r = this._radius, - r2 = this._radiusY || r, - w = this._clickTolerance(), - p = [r + w, r2 + w]; - this._pxBounds = new L.Bounds(this._point.subtract(p), this._point.add(p)); - }, - - _update: function () { - if (this._map) { - this._updatePath(); - } - }, - - _updatePath: function () { - this._renderer._updateCircle(this); - }, - - _empty: function () { - return this._radius && !this._renderer._bounds.intersects(this._pxBounds); - } -}); - -L.circleMarker = function (latlng, options) { - return new L.CircleMarker(latlng, options); -}; - - -/* - * L.Circle is a circle overlay (with a certain radius in meters). - * It's an approximation and starts to diverge from a real circle closer to poles (due to projection distortion) - */ - -L.Circle = L.CircleMarker.extend({ - - initialize: function (latlng, radius, options) { - L.setOptions(this, options); - this._latlng = L.latLng(latlng); - this._mRadius = radius; - }, - - setRadius: function (radius) { - this._mRadius = radius; - return this.redraw(); - }, - - getRadius: function () { - return this._mRadius; - }, - - getBounds: function () { - var half = [this._radius, this._radiusY]; - - return new L.LatLngBounds( - this._map.layerPointToLatLng(this._point.subtract(half)), - this._map.layerPointToLatLng(this._point.add(half))); - }, - - setStyle: L.Path.prototype.setStyle, + openPopup: function (latlng) { + + if (this._popup) { + // open the popup from one of the path's points if not specified + latlng = latlng || this._latlng || + this._latlngs[Math.floor(this._latlngs.length / 2)]; + + this._openPopup({latlng: latlng}); + } + + return this; + }, + + closePopup: function () { + if (this._popup) { + this._popup._close(); + } + return this; + }, + + _openPopup: function (e) { + this._popup.setLatLng(e.latlng); + this._map.openPopup(this._popup); + } +}); - _project: function () { - var lng = this._latlng.lng, - lat = this._latlng.lat, - map = this._map, - crs = map.options.crs; +/* + * Vector rendering for IE6-8 through VML. + * Thanks to Dmitry Baranovsky and his Raphael library for inspiration! + */ + +L.Browser.vml = !L.Browser.svg && (function () { + try { + var div = document.createElement('div'); + div.innerHTML = ''; + + var shape = div.firstChild; + shape.style.behavior = 'url(#default#VML)'; + + return shape && (typeof shape.adj === 'object'); + + } catch (e) { + return false; + } +}()); + +L.Path = L.Browser.svg || !L.Browser.vml ? L.Path : L.Path.extend({ + statics: { + VML: true, + CLIP_PADDING: 0.02 + }, + + _createElement: (function () { + try { + document.namespaces.add('lvml', 'urn:schemas-microsoft-com:vml'); + return function (name) { + return document.createElement(''); + }; + } catch (e) { + return function (name) { + return document.createElement( + '<' + name + ' xmlns="urn:schemas-microsoft.com:vml" class="lvml">'); + }; + } + }()), + + _initPath: function () { + var container = this._container = this._createElement('shape'); + + L.DomUtil.addClass(container, 'leaflet-vml-shape' + + (this.options.className ? ' ' + this.options.className : '')); + + if (this.options.clickable) { + L.DomUtil.addClass(container, 'leaflet-clickable'); + } + + container.coordsize = '1 1'; + + this._path = this._createElement('path'); + container.appendChild(this._path); + + this._map._pathRoot.appendChild(container); + }, + + _initStyle: function () { + this._updateStyle(); + }, + + _updateStyle: function () { + var stroke = this._stroke, + fill = this._fill, + options = this.options, + container = this._container; + + container.stroked = options.stroke; + container.filled = options.fill; + + if (options.stroke) { + if (!stroke) { + stroke = this._stroke = this._createElement('stroke'); + stroke.endcap = 'round'; + container.appendChild(stroke); + } + stroke.weight = options.weight + 'px'; + stroke.color = options.color; + stroke.opacity = options.opacity; + + if (options.dashArray) { + stroke.dashStyle = L.Util.isArray(options.dashArray) ? + options.dashArray.join(' ') : + options.dashArray.replace(/( *, *)/g, ' '); + } else { + stroke.dashStyle = ''; + } + if (options.lineCap) { + stroke.endcap = options.lineCap.replace('butt', 'flat'); + } + if (options.lineJoin) { + stroke.joinstyle = options.lineJoin; + } + + } else if (stroke) { + container.removeChild(stroke); + this._stroke = null; + } + + if (options.fill) { + if (!fill) { + fill = this._fill = this._createElement('fill'); + container.appendChild(fill); + } + fill.color = options.fillColor || options.color; + fill.opacity = options.fillOpacity; + + } else if (fill) { + container.removeChild(fill); + this._fill = null; + } + }, + + _updatePath: function () { + var style = this._container.style; + + style.display = 'none'; + this._path.v = this.getPathString() + ' '; // the space fixes IE empty path string bug + style.display = ''; + } +}); + +L.Map.include(L.Browser.svg || !L.Browser.vml ? {} : { + _initPathRoot: function () { + if (this._pathRoot) { return; } + + var root = this._pathRoot = document.createElement('div'); + root.className = 'leaflet-vml-container'; + this._panes.overlayPane.appendChild(root); + + this.on('moveend', this._updatePathViewport); + this._updatePathViewport(); + } +}); - if (crs.distance === L.CRS.Earth.distance) { - var d = Math.PI / 180, - latR = (this._mRadius / L.CRS.Earth.R) / d, - top = map.project([lat + latR, lng]), - bottom = map.project([lat - latR, lng]), - p = top.add(bottom).divideBy(2), - lat2 = map.unproject(p).lat, - lngR = Math.acos((Math.cos(latR * d) - Math.sin(lat * d) * Math.sin(lat2 * d)) / - (Math.cos(lat * d) * Math.cos(lat2 * d))) / d; - this._point = p.subtract(map.getPixelOrigin()); - this._radius = isNaN(lngR) ? 0 : Math.max(Math.round(p.x - map.project([lat2, lng - lngR]).x), 1); - this._radiusY = Math.max(Math.round(p.y - top.y), 1); +/* + * Vector rendering for all browsers that support canvas. + */ + +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: { + //CLIP_PADDING: 0.02, // not sure if there's a need to set it to a small value + CANVAS: true, + SVG: false + }, + + redraw: function () { + if (this._map) { + this.projectLatlngs(); + this._requestUpdate(); + } + return this; + }, + + setStyle: function (style) { + L.setOptions(this, style); + + if (this._map) { + this._updateStyle(); + this._requestUpdate(); + } + return this; + }, + + onRemove: function (map) { + map + .off('viewreset', this.projectLatlngs, this) + .off('moveend', this._updatePath, this); + + if (this.options.clickable) { + this._map.off('click', this._onClick, this); + this._map.off('mousemove', this._onMouseMove, this); + } + + this._requestUpdate(); + + this.fire('remove'); + this._map = null; + }, + + _requestUpdate: function () { + if (this._map && !L.Path._updateRequest) { + L.Path._updateRequest = L.Util.requestAnimFrame(this._fireMapMoveEnd, this._map); + } + }, + + _fireMapMoveEnd: function () { + L.Path._updateRequest = null; + this.fire('moveend'); + }, + + _initElements: function () { + this._map._initPathRoot(); + this._ctx = this._map._canvasCtx; + }, + + _updateStyle: function () { + var options = this.options; + + if (options.stroke) { + this._ctx.lineWidth = options.weight; + this._ctx.strokeStyle = options.color; + } + if (options.fill) { + this._ctx.fillStyle = options.fillColor || options.color; + } + }, + + _drawPath: function () { + var i, j, len, len2, point, drawMethod; + + this._ctx.beginPath(); + + for (i = 0, len = this._parts.length; i < len; i++) { + for (j = 0, len2 = this._parts[i].length; j < len2; j++) { + point = this._parts[i][j]; + drawMethod = (j === 0 ? 'move' : 'line') + 'To'; + + this._ctx[drawMethod](point.x, point.y); + } + // TODO refactor ugly hack + if (this instanceof L.Polygon) { + this._ctx.closePath(); + } + } + }, + + _checkIfEmpty: function () { + return !this._parts.length; + }, + + _updatePath: function () { + if (this._checkIfEmpty()) { return; } + + var ctx = this._ctx, + options = this.options; + + this._drawPath(); + ctx.save(); + this._updateStyle(); + + if (options.fill) { + ctx.globalAlpha = options.fillOpacity; + ctx.fill(); + } + + if (options.stroke) { + ctx.globalAlpha = options.opacity; + ctx.stroke(); + } + + ctx.restore(); + + // TODO optimization: 1 fill/stroke for all features with equal style instead of 1 for each feature + }, + + _initEvents: function () { + if (this.options.clickable) { + // TODO dblclick + this._map.on('mousemove', this._onMouseMove, this); + this._map.on('click', this._onClick, this); + } + }, + + _onClick: function (e) { + if (this._containsPoint(e.layerPoint)) { + this.fire('click', e); + } + }, + + _onMouseMove: function (e) { + if (!this._map || this._map._animatingZoom) { return; } + + // TODO don't do on each move + if (this._containsPoint(e.layerPoint)) { + this._ctx.canvas.style.cursor = 'pointer'; + this._mouseInside = true; + this.fire('mouseover', e); + + } else if (this._mouseInside) { + this._ctx.canvas.style.cursor = ''; + this._mouseInside = false; + this.fire('mouseout', e); + } + } +}); + +L.Map.include((L.Path.SVG && !window.L_PREFER_CANVAS) || !L.Browser.canvas ? {} : { + _initPathRoot: function () { + var root = this._pathRoot, + ctx; + + if (!root) { + root = this._pathRoot = document.createElement('canvas'); + root.style.position = 'absolute'; + ctx = this._canvasCtx = root.getContext('2d'); + + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + this._panes.overlayPane.appendChild(root); + + if (this.options.zoomAnimation) { + this._pathRoot.className = 'leaflet-zoom-animated'; + this.on('zoomanim', this._animatePathZoom); + this.on('zoomend', this._endPathZoom); + } + this.on('moveend', this._updateCanvasViewport); + this._updateCanvasViewport(); + } + }, + + _updateCanvasViewport: function () { + // don't redraw while zooming. See _updateSvgViewport for more details + if (this._pathZooming) { return; } + this._updatePathViewport(); + + var vp = this._pathViewport, + min = vp.min, + size = vp.max.subtract(min), + root = this._pathRoot; + + //TODO check if this works properly on mobile webkit + L.DomUtil.setPosition(root, min); + root.width = size.x; + root.height = size.y; + root.getContext('2d').translate(-min.x, -min.y); + } +}); - } else { - var latlng2 = crs.unproject(crs.project(this._latlng).subtract([this._mRadius, 0])); - this._point = map.latLngToLayerPoint(this._latlng); - this._radius = this._point.x - map.latLngToLayerPoint(latlng2).x; - } - - this._updateBounds(); - } -}); - -L.circle = function (latlng, radius, options) { - return new L.Circle(latlng, radius, options); -}; - - -/* - * L.SVG renders vector layers with SVG. All SVG-specific code goes here. - */ - -L.SVG = L.Renderer.extend({ - - _initContainer: function () { - this._container = L.SVG.create('svg'); - - this._paths = {}; - this._initEvents(); - - // makes it possible to click through svg root; we'll reset it back in individual paths - this._container.setAttribute('pointer-events', 'none'); - }, - - _update: function () { - if (this._map._animatingZoom && this._bounds) { return; } - - L.Renderer.prototype._update.call(this); - - var b = this._bounds, - size = b.getSize(), - container = this._container, - pane = this.getPane(); - - // hack to make flicker on drag end on mobile webkit less irritating - if (L.Browser.mobileWebkit) { - pane.removeChild(container); - } - - L.DomUtil.setPosition(container, b.min); - - // set size of svg-container if changed - if (!this._svgSize || !this._svgSize.equals(size)) { - this._svgSize = size; - container.setAttribute('width', size.x); - container.setAttribute('height', size.y); - } - - // movement: update container viewBox so that we don't have to change coordinates of individual layers - L.DomUtil.setPosition(container, b.min); - container.setAttribute('viewBox', [b.min.x, b.min.y, size.x, size.y].join(' ')); - - if (L.Browser.mobileWebkit) { - pane.appendChild(container); - } - }, - - // methods below are called by vector layers implementations - - _initPath: function (layer) { - var path = layer._path = L.SVG.create('path'); - - if (layer.options.className) { - L.DomUtil.addClass(path, layer.options.className); - } - - if (layer.options.interactive) { - L.DomUtil.addClass(path, 'leaflet-interactive'); - } - - this._updateStyle(layer); - }, - - _addPath: function (layer) { - var path = layer._path; - this._container.appendChild(path); - this._paths[L.stamp(path)] = layer; - }, - - _removePath: function (layer) { - var path = layer._path; - L.DomUtil.remove(path); - delete this._paths[L.stamp(path)]; - }, - - _updatePath: function (layer) { - layer._project(); - layer._update(); - }, - - _updateStyle: function (layer) { - var path = layer._path, - options = layer.options; - - if (!path) { return; } - - if (options.stroke) { - path.setAttribute('stroke', options.color); - path.setAttribute('stroke-opacity', options.opacity); - path.setAttribute('stroke-width', options.weight); - path.setAttribute('stroke-linecap', options.lineCap); - path.setAttribute('stroke-linejoin', options.lineJoin); - - if (options.dashArray) { - path.setAttribute('stroke-dasharray', options.dashArray); - } else { - path.removeAttribute('stroke-dasharray'); - } - - if (options.dashOffset) { - path.setAttribute('stroke-dashoffset', options.dashOffset); - } else { - path.removeAttribute('stroke-dashoffset'); - } - } else { - path.setAttribute('stroke', 'none'); - } - - if (options.fill) { - path.setAttribute('fill', options.fillColor || options.color); - path.setAttribute('fill-opacity', options.fillOpacity); - path.setAttribute('fill-rule', 'evenodd'); - } else { - path.setAttribute('fill', 'none'); - } - - path.setAttribute('pointer-events', options.pointerEvents || (options.interactive ? 'visiblePainted' : 'none')); - }, - - _updatePoly: function (layer, closed) { - this._setPath(layer, L.SVG.pointsToPath(layer._parts, closed)); - }, - - _updateCircle: function (layer) { - var p = layer._point, - r = layer._radius, - r2 = layer._radiusY || r, - arc = 'a' + r + ',' + r2 + ' 0 1,0 '; - - // drawing a circle with two half-arcs - var d = layer._empty() ? 'M0 0' : - 'M' + (p.x - r) + ',' + p.y + - arc + (r * 2) + ',0 ' + - arc + (-r * 2) + ',0 '; - - this._setPath(layer, d); - }, - - _setPath: function (layer, path) { - layer._path.setAttribute('d', path); - }, - - // SVG does not have the concept of zIndex so we resort to changing the DOM order of elements - _bringToFront: function (layer) { - L.DomUtil.toFront(layer._path); - }, - - _bringToBack: function (layer) { - L.DomUtil.toBack(layer._path); - }, - - // TODO remove duplication with L.Map - _initEvents: function () { - L.DomEvent.on(this._container, 'click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu', - this._fireMouseEvent, this); - }, - - _fireMouseEvent: function (e) { - var path = this._paths[L.stamp(e.target || e.srcElement)]; - if (path) { - path._fireMouseEvent(e); - } - } -}); - - -L.extend(L.SVG, { - create: function (name) { - return document.createElementNS('http://www.w3.org/2000/svg', name); - }, - - // generates SVG path string for multiple rings, with each ring turning into "M..L..L.." instructions - pointsToPath: function (rings, closed) { - var str = '', - i, j, len, len2, points, p; - - for (i = 0, len = rings.length; i < len; i++) { - points = rings[i]; - - for (j = 0, len2 = points.length; j < len2; j++) { - p = points[j]; - str += (j ? 'L' : 'M') + p.x + ' ' + p.y; - } - - // closes the ring for polygons; "x" is VML syntax - str += closed ? (L.Browser.svg ? 'z' : 'x') : ''; - } - - // SVG complains about empty path strings - return str || 'M0 0'; - } -}); - -L.Browser.svg = !!(document.createElementNS && L.SVG.create('svg').createSVGRect); - -L.svg = function (options) { - return L.Browser.svg || L.Browser.vml ? new L.SVG(options) : null; -}; - - -/* - * Vector rendering for IE7-8 through VML. - * Thanks to Dmitry Baranovsky and his Raphael library for inspiration! - */ - -L.Browser.vml = !L.Browser.svg && (function () { - try { - var div = document.createElement('div'); - div.innerHTML = ''; - - var shape = div.firstChild; - shape.style.behavior = 'url(#default#VML)'; - - return shape && (typeof shape.adj === 'object'); - - } catch (e) { - return false; - } -}()); - -// redefine some SVG methods to handle VML syntax which is similar but with some differences -L.SVG.include(!L.Browser.vml ? {} : { - - _initContainer: function () { - this._container = L.DomUtil.create('div', 'leaflet-vml-container'); - - this._paths = {}; - this._initEvents(); - }, - - _update: function () { - if (this._map._animatingZoom) { return; } - L.Renderer.prototype._update.call(this); - }, - - _initPath: function (layer) { - var container = layer._container = L.SVG.create('shape'); - - L.DomUtil.addClass(container, 'leaflet-vml-shape ' + (this.options.className || '')); - - container.coordsize = '1 1'; - - layer._path = L.SVG.create('path'); - container.appendChild(layer._path); - - this._updateStyle(layer); - }, - - _addPath: function (layer) { - var container = layer._container; - this._container.appendChild(container); - this._paths[L.stamp(container)] = layer; - }, - - _removePath: function (layer) { - var container = layer._container; - L.DomUtil.remove(container); - delete this._paths[L.stamp(container)]; - }, - - _updateStyle: function (layer) { - var stroke = layer._stroke, - fill = layer._fill, - options = layer.options, - container = layer._container; - - container.stroked = !!options.stroke; - container.filled = !!options.fill; - - if (options.stroke) { - if (!stroke) { - stroke = layer._stroke = L.SVG.create('stroke'); - container.appendChild(stroke); - } - stroke.weight = options.weight + 'px'; - stroke.color = options.color; - stroke.opacity = options.opacity; - - if (options.dashArray) { - stroke.dashStyle = L.Util.isArray(options.dashArray) ? - options.dashArray.join(' ') : - options.dashArray.replace(/( *, *)/g, ' '); - } else { - stroke.dashStyle = ''; - } - stroke.endcap = options.lineCap.replace('butt', 'flat'); - stroke.joinstyle = options.lineJoin; - - } else if (stroke) { - container.removeChild(stroke); - layer._stroke = null; - } - - if (options.fill) { - if (!fill) { - fill = layer._fill = L.SVG.create('fill'); - container.appendChild(fill); - } - fill.color = options.fillColor || options.color; - fill.opacity = options.fillOpacity; - - } else if (fill) { - container.removeChild(fill); - layer._fill = null; - } - }, - - _updateCircle: function (layer) { - var p = layer._point.round(), - r = Math.round(layer._radius), - r2 = Math.round(layer._radiusY || r); - - this._setPath(layer, layer._empty() ? 'M0 0' : - 'AL ' + p.x + ',' + p.y + ' ' + r + ',' + r2 + ' 0,' + (65535 * 360)); - }, - - _setPath: function (layer, path) { - layer._path.v = path; - } -}); - -if (L.Browser.vml) { - L.SVG.create = (function () { - try { - document.namespaces.add('lvml', 'urn:schemas-microsoft-com:vml'); - return function (name) { - return document.createElement(''); - }; - } catch (e) { - return function (name) { - return document.createElement('<' + name + ' xmlns="urn:schemas-microsoft.com:vml" class="lvml">'); - }; - } - })(); -} - - -/* - * L.Canvas handles Canvas vector layers rendering and mouse events handling. All Canvas-specific code goes here. - */ - -L.Canvas = L.Renderer.extend({ - - onAdd: function () { - L.Renderer.prototype.onAdd.call(this); - - this._layers = this._layers || {}; - - // redraw vectors since canvas is cleared upon removal - this._draw(); - }, - - _initContainer: function () { - var container = this._container = document.createElement('canvas'); - - L.DomEvent - .on(container, 'mousemove', this._onMouseMove, this) - .on(container, 'click dblclick mousedown mouseup contextmenu', this._onClick, this); - - this._ctx = container.getContext('2d'); - }, - - _update: function () { - if (this._map._animatingZoom && this._bounds) { return; } - - L.Renderer.prototype._update.call(this); - - var b = this._bounds, - container = this._container, - size = b.getSize(), - m = L.Browser.retina ? 2 : 1; - - L.DomUtil.setPosition(container, b.min); - - // set canvas size (also clearing it); use double size on retina - container.width = m * size.x; - container.height = m * size.y; - container.style.width = size.x + 'px'; - container.style.height = size.y + 'px'; - - if (L.Browser.retina) { - this._ctx.scale(2, 2); - } - - // translate so we use the same path coordinates after canvas element moves - this._ctx.translate(-b.min.x, -b.min.y); - }, - - _initPath: function (layer) { - this._layers[L.stamp(layer)] = layer; - }, - - _addPath: L.Util.falseFn, - - _removePath: function (layer) { - layer._removed = true; - this._requestRedraw(layer); - }, - - _updatePath: function (layer) { - this._redrawBounds = layer._pxBounds; - this._draw(true); - layer._project(); - layer._update(); - this._draw(); - this._redrawBounds = null; - }, - - _updateStyle: function (layer) { - this._requestRedraw(layer); - }, - - _requestRedraw: function (layer) { - if (!this._map) { return; } - - this._redrawBounds = this._redrawBounds || new L.Bounds(); - this._redrawBounds.extend(layer._pxBounds.min).extend(layer._pxBounds.max); - - this._redrawRequest = this._redrawRequest || L.Util.requestAnimFrame(this._redraw, this); - }, - - _redraw: function () { - this._redrawRequest = null; - - this._draw(true); // clear layers in redraw bounds - this._draw(); // draw layers - - this._redrawBounds = null; - }, - - _draw: function (clear) { - this._clear = clear; - var layer; - - for (var id in this._layers) { - layer = this._layers[id]; - if (!this._redrawBounds || layer._pxBounds.intersects(this._redrawBounds)) { - layer._updatePath(); - } - if (clear && layer._removed) { - delete layer._removed; - delete this._layers[id]; - } - } - }, - - _updatePoly: function (layer, closed) { - - var i, j, len2, p, - parts = layer._parts, - len = parts.length, - ctx = this._ctx; - - if (!len) { return; } - - ctx.beginPath(); - - for (i = 0; i < len; i++) { - for (j = 0, len2 = parts[i].length; j < len2; j++) { - p = parts[i][j]; - ctx[j ? 'lineTo' : 'moveTo'](p.x, p.y); - } - if (closed) { - ctx.closePath(); - } - } - - this._fillStroke(ctx, layer); - - // TODO optimization: 1 fill/stroke for all features with equal style instead of 1 for each feature - }, - - _updateCircle: function (layer) { - - if (layer._empty()) { return; } - - var p = layer._point, - ctx = this._ctx, - r = layer._radius, - s = (layer._radiusY || r) / r; - - if (s !== 1) { - ctx.save(); - ctx.scale(1, s); - } - - ctx.beginPath(); - ctx.arc(p.x, p.y / s, r, 0, Math.PI * 2, false); - - if (s !== 1) { - ctx.restore(); - } - - this._fillStroke(ctx, layer); - }, - - _fillStroke: function (ctx, layer) { - var clear = this._clear, - options = layer.options; - - ctx.globalCompositeOperation = clear ? 'destination-out' : 'source-over'; - - if (options.fill) { - ctx.globalAlpha = clear ? 1 : options.fillOpacity; - ctx.fillStyle = options.fillColor || options.color; - ctx.fill('evenodd'); - } - - if (options.stroke) { - ctx.globalAlpha = clear ? 1 : options.opacity; - - // if clearing shape, do it with the previously drawn line width - layer._prevWeight = ctx.lineWidth = clear ? layer._prevWeight + 1 : options.weight; +/* + * L.LineUtil contains different utility functions for line segments + * and polylines (clipping, simplification, distances, etc.) + */ + +/*jshint bitwise:false */ // allow bitwise operations for this file + +L.LineUtil = { + + // Simplify polyline with vertex reduction and Douglas-Peucker simplification. + // Improves rendering performance dramatically by lessening the number of points to draw. + + simplify: function (/*Point[]*/ points, /*Number*/ tolerance) { + if (!tolerance || !points.length) { + return points.slice(); + } + + var sqTolerance = tolerance * tolerance; + + // stage 1: vertex reduction + points = this._reducePoints(points, sqTolerance); + + // stage 2: Douglas-Peucker simplification + points = this._simplifyDP(points, sqTolerance); + + return points; + }, + + // distance from a point to a segment between two points + pointToSegmentDistance: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2) { + return Math.sqrt(this._sqClosestPointOnSegment(p, p1, p2, true)); + }, + + closestPointOnSegment: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2) { + return this._sqClosestPointOnSegment(p, p1, p2); + }, + + // Douglas-Peucker simplification, see http://en.wikipedia.org/wiki/Douglas-Peucker_algorithm + _simplifyDP: function (points, sqTolerance) { + + var len = points.length, + ArrayConstructor = typeof Uint8Array !== undefined + '' ? Uint8Array : Array, + markers = new ArrayConstructor(len); + + markers[0] = markers[len - 1] = 1; + + this._simplifyDPStep(points, markers, sqTolerance, 0, len - 1); + + var i, + newPoints = []; + + for (i = 0; i < len; i++) { + if (markers[i]) { + newPoints.push(points[i]); + } + } + + return newPoints; + }, + + _simplifyDPStep: function (points, markers, sqTolerance, first, last) { + + var maxSqDist = 0, + index, i, sqDist; + + for (i = first + 1; i <= last - 1; i++) { + sqDist = this._sqClosestPointOnSegment(points[i], points[first], points[last], true); + + if (sqDist > maxSqDist) { + index = i; + maxSqDist = sqDist; + } + } + + if (maxSqDist > sqTolerance) { + markers[index] = 1; + + this._simplifyDPStep(points, markers, sqTolerance, first, index); + this._simplifyDPStep(points, markers, sqTolerance, index, last); + } + }, + + // reduce points that are too close to each other to a single point + _reducePoints: function (points, sqTolerance) { + var reducedPoints = [points[0]]; + + for (var i = 1, prev = 0, len = points.length; i < len; i++) { + if (this._sqDist(points[i], points[prev]) > sqTolerance) { + reducedPoints.push(points[i]); + prev = i; + } + } + if (prev < len - 1) { + reducedPoints.push(points[len - 1]); + } + return reducedPoints; + }, + + // Cohen-Sutherland line clipping algorithm. + // Used to avoid rendering parts of a polyline that are not currently visible. + + clipSegment: function (a, b, bounds, useLastCode) { + var codeA = useLastCode ? this._lastCode : this._getBitCode(a, bounds), + codeB = this._getBitCode(b, bounds), + + codeOut, p, newCode; + + // save 2nd code to avoid calculating it on the next segment + this._lastCode = codeB; + + while (true) { + // if a,b is inside the clip window (trivial accept) + if (!(codeA | codeB)) { + return [a, b]; + // if a,b is outside the clip window (trivial reject) + } else if (codeA & codeB) { + return false; + // other cases + } else { + codeOut = codeA || codeB; + p = this._getEdgeIntersection(a, b, codeOut, bounds); + newCode = this._getBitCode(p, bounds); + + if (codeOut === codeA) { + a = p; + codeA = newCode; + } else { + b = p; + codeB = newCode; + } + } + } + }, + + _getEdgeIntersection: function (a, b, code, bounds) { + var dx = b.x - a.x, + dy = b.y - a.y, + min = bounds.min, + max = bounds.max; + + if (code & 8) { // top + return new L.Point(a.x + dx * (max.y - a.y) / dy, max.y); + } else if (code & 4) { // bottom + return new L.Point(a.x + dx * (min.y - a.y) / dy, min.y); + } else if (code & 2) { // right + return new L.Point(max.x, a.y + dy * (max.x - a.x) / dx); + } else if (code & 1) { // left + return new L.Point(min.x, a.y + dy * (min.x - a.x) / dx); + } + }, + + _getBitCode: function (/*Point*/ p, bounds) { + var code = 0; + + if (p.x < bounds.min.x) { // left + code |= 1; + } else if (p.x > bounds.max.x) { // right + code |= 2; + } + if (p.y < bounds.min.y) { // bottom + code |= 4; + } else if (p.y > bounds.max.y) { // top + code |= 8; + } + + return code; + }, + + // square distance (to avoid unnecessary Math.sqrt calls) + _sqDist: function (p1, p2) { + var dx = p2.x - p1.x, + dy = p2.y - p1.y; + return dx * dx + dy * dy; + }, + + // return closest point on segment or distance to that point + _sqClosestPointOnSegment: function (p, p1, p2, sqDist) { + var x = p1.x, + y = p1.y, + dx = p2.x - x, + dy = p2.y - y, + dot = dx * dx + dy * dy, + t; + + if (dot > 0) { + t = ((p.x - x) * dx + (p.y - y) * dy) / dot; + + if (t > 1) { + x = p2.x; + y = p2.y; + } else if (t > 0) { + x += dx * t; + y += dy * t; + } + } + + dx = p.x - x; + dy = p.y - y; + + return sqDist ? dx * dx + dy * dy : new L.Point(x, y); + } +}; - ctx.strokeStyle = options.color; - ctx.lineCap = options.lineCap; - ctx.lineJoin = options.lineJoin; - ctx.stroke(); - } - }, - // Canvas obviously doesn't have mouse events for individual drawn objects, - // so we emulate that by calculating what's under the mouse on mousemove/click manually +/* + * L.Polyline is used to display polylines on a map. + */ + +L.Polyline = L.Path.extend({ + initialize: function (latlngs, options) { + L.Path.prototype.initialize.call(this, options); + + this._latlngs = this._convertLatLngs(latlngs); + }, + + options: { + // how much to simplify the polyline on each zoom level + // more = better performance and smoother look, less = more accurate + smoothFactor: 1.0, + noClip: false + }, + + projectLatlngs: function () { + this._originalPoints = []; + + for (var i = 0, len = this._latlngs.length; i < len; i++) { + this._originalPoints[i] = this._map.latLngToLayerPoint(this._latlngs[i]); + } + }, + + getPathString: function () { + for (var i = 0, len = this._parts.length, str = ''; i < len; i++) { + str += this._getPathPartStr(this._parts[i]); + } + return str; + }, + + getLatLngs: function () { + return this._latlngs; + }, + + setLatLngs: function (latlngs) { + this._latlngs = this._convertLatLngs(latlngs); + return this.redraw(); + }, + + addLatLng: function (latlng) { + this._latlngs.push(L.latLng(latlng)); + return this.redraw(); + }, + + spliceLatLngs: function () { // (Number index, Number howMany) + var removed = [].splice.apply(this._latlngs, arguments); + this._convertLatLngs(this._latlngs, true); + this.redraw(); + return removed; + }, + + closestLayerPoint: function (p) { + var minDistance = Infinity, parts = this._parts, p1, p2, minPoint = null; + + for (var j = 0, jLen = parts.length; j < jLen; j++) { + var points = parts[j]; + for (var i = 1, len = points.length; i < len; i++) { + p1 = points[i - 1]; + p2 = points[i]; + var sqDist = L.LineUtil._sqClosestPointOnSegment(p, p1, p2, true); + if (sqDist < minDistance) { + minDistance = sqDist; + minPoint = L.LineUtil._sqClosestPointOnSegment(p, p1, p2); + } + } + } + if (minPoint) { + minPoint.distance = Math.sqrt(minDistance); + } + return minPoint; + }, + + getBounds: function () { + return new L.LatLngBounds(this.getLatLngs()); + }, + + _convertLatLngs: function (latlngs, overwrite) { + var i, len, target = overwrite ? latlngs : []; + + for (i = 0, len = latlngs.length; i < len; i++) { + if (L.Util.isArray(latlngs[i]) && typeof latlngs[i][0] !== 'number') { + return; + } + target[i] = L.latLng(latlngs[i]); + } + return target; + }, + + _initEvents: function () { + L.Path.prototype._initEvents.call(this); + }, + + _getPathPartStr: function (points) { + var round = L.Path.VML; + + for (var j = 0, len2 = points.length, str = '', p; j < len2; j++) { + p = points[j]; + if (round) { + p._round(); + } + str += (j ? 'L' : 'M') + p.x + ' ' + p.y; + } + return str; + }, + + _clipPoints: function () { + var points = this._originalPoints, + len = points.length, + i, k, segment; + + if (this.options.noClip) { + this._parts = [points]; + return; + } + + this._parts = []; + + var parts = this._parts, + vp = this._map._pathViewport, + lu = L.LineUtil; + + for (i = 0, k = 0; i < len - 1; i++) { + segment = lu.clipSegment(points[i], points[i + 1], vp, i); + if (!segment) { + continue; + } + + parts[k] = parts[k] || []; + parts[k].push(segment[0]); + + // if segment goes out of screen, or it's the last one, it's the end of the line part + if ((segment[1] !== points[i + 1]) || (i === len - 2)) { + parts[k].push(segment[1]); + k++; + } + } + }, + + // simplify each clipped part of the polyline + _simplifyPoints: function () { + var parts = this._parts, + lu = L.LineUtil; + + for (var i = 0, len = parts.length; i < len; i++) { + parts[i] = lu.simplify(parts[i], this.options.smoothFactor); + } + }, + + _updatePath: function () { + if (!this._map) { return; } + + this._clipPoints(); + this._simplifyPoints(); + + L.Path.prototype._updatePath.call(this); + } +}); + +L.polyline = function (latlngs, options) { + return new L.Polyline(latlngs, options); +}; - _onClick: function (e) { - var point = this._map.mouseEventToLayerPoint(e); - for (var id in this._layers) { - if (this._layers[id]._containsPoint(point)) { - this._layers[id]._fireMouseEvent(e); - } - } - }, +/* + * L.PolyUtil contains utility functions for polygons (clipping, etc.). + */ + +/*jshint bitwise:false */ // allow bitwise operations here + +L.PolyUtil = {}; + +/* + * Sutherland-Hodgeman polygon clipping algorithm. + * Used to avoid rendering parts of a polygon that are not currently visible. + */ +L.PolyUtil.clipPolygon = function (points, bounds) { + var clippedPoints, + edges = [1, 4, 2, 8], + i, j, k, + a, b, + len, edge, p, + lu = L.LineUtil; + + for (i = 0, len = points.length; i < len; i++) { + points[i]._code = lu._getBitCode(points[i], bounds); + } + + // for each edge (left, bottom, right, top) + for (k = 0; k < 4; k++) { + edge = edges[k]; + clippedPoints = []; + + for (i = 0, len = points.length, j = len - 1; i < len; j = i++) { + a = points[i]; + b = points[j]; + + // if a is inside the clip window + if (!(a._code & edge)) { + // if b is outside the clip window (a->b goes out of screen) + if (b._code & edge) { + p = lu._getEdgeIntersection(b, a, edge, bounds); + p._code = lu._getBitCode(p, bounds); + clippedPoints.push(p); + } + clippedPoints.push(a); + + // else if b is inside the clip window (a->b enters the screen) + } else if (!(b._code & edge)) { + p = lu._getEdgeIntersection(b, a, edge, bounds); + p._code = lu._getBitCode(p, bounds); + clippedPoints.push(p); + } + } + points = clippedPoints; + } + + return points; +}; - _onMouseMove: function (e) { - if (!this._map || this._map._animatingZoom) { return; } - var point = this._map.mouseEventToLayerPoint(e); +/* + * L.Polygon is used to display polygons on a map. + */ + +L.Polygon = L.Polyline.extend({ + options: { + fill: true + }, + + initialize: function (latlngs, options) { + L.Polyline.prototype.initialize.call(this, latlngs, options); + this._initWithHoles(latlngs); + }, + + _initWithHoles: function (latlngs) { + var i, len, hole; + if (latlngs && L.Util.isArray(latlngs[0]) && (typeof latlngs[0][0] !== 'number')) { + this._latlngs = this._convertLatLngs(latlngs[0]); + this._holes = latlngs.slice(1); + + for (i = 0, len = this._holes.length; i < len; i++) { + hole = this._holes[i] = this._convertLatLngs(this._holes[i]); + if (hole[0].equals(hole[hole.length - 1])) { + hole.pop(); + } + } + } + + // filter out last point if its equal to the first one + latlngs = this._latlngs; + + if (latlngs.length >= 2 && latlngs[0].equals(latlngs[latlngs.length - 1])) { + latlngs.pop(); + } + }, + + projectLatlngs: function () { + L.Polyline.prototype.projectLatlngs.call(this); + + // project polygon holes points + // TODO move this logic to Polyline to get rid of duplication + this._holePoints = []; + + if (!this._holes) { return; } + + var i, j, len, len2; + + for (i = 0, len = this._holes.length; i < len; i++) { + this._holePoints[i] = []; + + for (j = 0, len2 = this._holes[i].length; j < len2; j++) { + this._holePoints[i][j] = this._map.latLngToLayerPoint(this._holes[i][j]); + } + } + }, + + setLatLngs: function (latlngs) { + if (latlngs && L.Util.isArray(latlngs[0]) && (typeof latlngs[0][0] !== 'number')) { + this._initWithHoles(latlngs); + return this.redraw(); + } else { + return L.Polyline.prototype.setLatLngs.call(this, latlngs); + } + }, + + _clipPoints: function () { + var points = this._originalPoints, + newParts = []; + + this._parts = [points].concat(this._holePoints); + + if (this.options.noClip) { return; } + + for (var i = 0, len = this._parts.length; i < len; i++) { + var clipped = L.PolyUtil.clipPolygon(this._parts[i], this._map._pathViewport); + if (clipped.length) { + newParts.push(clipped); + } + } + + this._parts = newParts; + }, + + _getPathPartStr: function (points) { + var str = L.Polyline.prototype._getPathPartStr.call(this, points); + return str + (L.Browser.svg ? 'z' : 'x'); + } +}); + +L.polygon = function (latlngs, options) { + return new L.Polygon(latlngs, options); +}; - // TODO don't do on each move event, throttle since it's expensive - for (var id in this._layers) { - this._handleHover(this._layers[id], e, point); - } - }, - _handleHover: function (layer, e, point) { - if (!layer.options.interactive) { return; } +/* + * Contains L.MultiPolyline and L.MultiPolygon layers. + */ + +(function () { + function createMulti(Klass) { + + return L.FeatureGroup.extend({ + + initialize: function (latlngs, options) { + this._layers = {}; + this._options = options; + this.setLatLngs(latlngs); + }, + + setLatLngs: function (latlngs) { + var i = 0, + len = latlngs.length; + + this.eachLayer(function (layer) { + if (i < len) { + layer.setLatLngs(latlngs[i++]); + } else { + this.removeLayer(layer); + } + }, this); + + while (i < len) { + this.addLayer(new Klass(latlngs[i++], this._options)); + } + + return this; + }, + + getLatLngs: function () { + var latlngs = []; + + this.eachLayer(function (layer) { + latlngs.push(layer.getLatLngs()); + }); + + return latlngs; + } + }); + } + + L.MultiPolyline = createMulti(L.Polyline); + L.MultiPolygon = createMulti(L.Polygon); + + L.multiPolyline = function (latlngs, options) { + return new L.MultiPolyline(latlngs, options); + }; + + L.multiPolygon = function (latlngs, options) { + return new L.MultiPolygon(latlngs, options); + }; +}()); - if (layer._containsPoint(point)) { - // if we just got inside the layer, fire mouseover - if (!layer._mouseInside) { - L.DomUtil.addClass(this._container, 'leaflet-interactive'); // change cursor - layer._fireMouseEvent(e, 'mouseover'); - layer._mouseInside = true; - } - // fire mousemove - layer._fireMouseEvent(e); - - } else if (layer._mouseInside) { - // if we're leaving the layer, fire mouseout - L.DomUtil.removeClass(this._container, 'leaflet-interactive'); - layer._fireMouseEvent(e, 'mouseout'); - layer._mouseInside = false; - } - }, - // TODO _bringToFront & _bringToBack, pretty tricky +/* + * L.Rectangle extends Polygon and creates a rectangle when passed a LatLngBounds object. + */ + +L.Rectangle = L.Polygon.extend({ + initialize: function (latLngBounds, options) { + L.Polygon.prototype.initialize.call(this, this._boundsToLatLngs(latLngBounds), options); + }, + + setBounds: function (latLngBounds) { + this.setLatLngs(this._boundsToLatLngs(latLngBounds)); + }, + + _boundsToLatLngs: function (latLngBounds) { + latLngBounds = L.latLngBounds(latLngBounds); + return [ + latLngBounds.getSouthWest(), + latLngBounds.getNorthWest(), + latLngBounds.getNorthEast(), + latLngBounds.getSouthEast() + ]; + } +}); + +L.rectangle = function (latLngBounds, options) { + return new L.Rectangle(latLngBounds, options); +}; - _bringToFront: L.Util.falseFn, - _bringToBack: L.Util.falseFn -}); -L.Browser.canvas = (function () { - return !!document.createElement('canvas').getContext; -}()); +/* + * L.Circle is a circle overlay (with a certain radius in meters). + */ + +L.Circle = L.Path.extend({ + initialize: function (latlng, radius, options) { + L.Path.prototype.initialize.call(this, options); + + this._latlng = L.latLng(latlng); + this._mRadius = radius; + }, + + options: { + fill: true + }, + + setLatLng: function (latlng) { + this._latlng = L.latLng(latlng); + return this.redraw(); + }, + + setRadius: function (radius) { + this._mRadius = radius; + return this.redraw(); + }, + + projectLatlngs: function () { + var lngRadius = this._getLngRadius(), + latlng = this._latlng, + pointLeft = this._map.latLngToLayerPoint([latlng.lat, latlng.lng - lngRadius]); + + this._point = this._map.latLngToLayerPoint(latlng); + this._radius = Math.max(this._point.x - pointLeft.x, 1); + }, + + getBounds: function () { + var lngRadius = this._getLngRadius(), + latRadius = (this._mRadius / 40075017) * 360, + latlng = this._latlng; + + return new L.LatLngBounds( + [latlng.lat - latRadius, latlng.lng - lngRadius], + [latlng.lat + latRadius, latlng.lng + lngRadius]); + }, + + getLatLng: function () { + return this._latlng; + }, + + getPathString: function () { + var p = this._point, + r = this._radius; + + if (this._checkIfEmpty()) { + return ''; + } + + if (L.Browser.svg) { + return 'M' + p.x + ',' + (p.y - r) + + 'A' + r + ',' + r + ',0,1,1,' + + (p.x - 0.1) + ',' + (p.y - r) + ' z'; + } else { + p._round(); + r = Math.round(r); + return 'AL ' + p.x + ',' + p.y + ' ' + r + ',' + r + ' 0,' + (65535 * 360); + } + }, + + getRadius: function () { + return this._mRadius; + }, + + // TODO Earth hardcoded, move into projection code! + + _getLatRadius: function () { + return (this._mRadius / 40075017) * 360; + }, + + _getLngRadius: function () { + return this._getLatRadius() / Math.cos(L.LatLng.DEG_TO_RAD * this._latlng.lat); + }, + + _checkIfEmpty: function () { + if (!this._map) { + return false; + } + var vp = this._map._pathViewport, + r = this._radius, + p = this._point; + + return p.x - r > vp.max.x || p.y - r > vp.max.y || + p.x + r < vp.min.x || p.y + r < vp.min.y; + } +}); + +L.circle = function (latlng, radius, options) { + return new L.Circle(latlng, radius, options); +}; -L.canvas = function (options) { - return L.Browser.canvas ? new L.Canvas(options) : null; -}; -L.Polyline.prototype._containsPoint = function (p, closed) { - var i, j, k, len, len2, part, - w = this._clickTolerance(); +/* + * L.CircleMarker is a circle overlay with a permanent pixel radius. + */ + +L.CircleMarker = L.Circle.extend({ + options: { + radius: 10, + weight: 2 + }, + + initialize: function (latlng, options) { + L.Circle.prototype.initialize.call(this, latlng, null, options); + this._radius = this.options.radius; + }, + + projectLatlngs: function () { + this._point = this._map.latLngToLayerPoint(this._latlng); + }, + + _updateStyle : function () { + L.Circle.prototype._updateStyle.call(this); + this.setRadius(this.options.radius); + }, + + setLatLng: function (latlng) { + L.Circle.prototype.setLatLng.call(this, latlng); + if (this._popup && this._popup._isOpen) { + this._popup.setLatLng(latlng); + } + return this; + }, + + setRadius: function (radius) { + this.options.radius = this._radius = radius; + return this.redraw(); + }, + + getRadius: function () { + return this._radius; + } +}); + +L.circleMarker = function (latlng, options) { + return new L.CircleMarker(latlng, options); +}; - if (!this._pxBounds.contains(p)) { return false; } - // hit detection for polylines - for (i = 0, len = this._parts.length; i < len; i++) { - part = this._parts[i]; +/* + * Extends L.Polyline to be able to manually detect clicks on Canvas-rendered polylines. + */ + +L.Polyline.include(!L.Path.CANVAS ? {} : { + _containsPoint: function (p, closed) { + var i, j, k, len, len2, dist, part, + w = this.options.weight / 2; + + if (L.Browser.touch) { + w += 10; // polyline click tolerance on touch devices + } + + for (i = 0, len = this._parts.length; i < len; i++) { + part = this._parts[i]; + for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) { + if (!closed && (j === 0)) { + continue; + } + + dist = L.LineUtil.pointToSegmentDistance(p, part[k], part[j]); + + if (dist <= w) { + return true; + } + } + } + return false; + } +}); - for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) { - if (!closed && (j === 0)) { continue; } - if (L.LineUtil.pointToSegmentDistance(p, part[k], part[j]) <= w) { - return true; - } - } - } - return false; -}; +/* + * Extends L.Polygon to be able to manually detect clicks on Canvas-rendered polygons. + */ + +L.Polygon.include(!L.Path.CANVAS ? {} : { + _containsPoint: function (p) { + var inside = false, + part, p1, p2, + i, j, k, + len, len2; + + // TODO optimization: check if within bounds first + + if (L.Polyline.prototype._containsPoint.call(this, p, true)) { + // click on polygon border + return true; + } + + // ray casting algorithm for detecting if point is in polygon + + for (i = 0, len = this._parts.length; i < len; i++) { + part = this._parts[i]; + + for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) { + p1 = part[j]; + p2 = part[k]; + + if (((p1.y > p.y) !== (p2.y > p.y)) && + (p.x < (p2.x - p1.x) * (p.y - p1.y) / (p2.y - p1.y) + p1.x)) { + inside = !inside; + } + } + } + + return inside; + } +}); -L.Polygon.prototype._containsPoint = function (p) { - var inside = false, - part, p1, p2, i, j, k, len, len2; - if (!this._pxBounds.contains(p)) { return false; } +/* + * Extends L.Circle with Canvas-specific code. + */ + +L.Circle.include(!L.Path.CANVAS ? {} : { + _drawPath: function () { + var p = this._point; + this._ctx.beginPath(); + this._ctx.arc(p.x, p.y, this._radius, 0, Math.PI * 2, false); + }, + + _containsPoint: function (p) { + var center = this._point, + w2 = this.options.stroke ? this.options.weight / 2 : 0; + + return (p.distanceTo(center) <= this._radius + w2); + } +}); - // ray casting algorithm for detecting if point is in polygon - for (i = 0, len = this._parts.length; i < len; i++) { - part = this._parts[i]; - for (j = 0, len2 = part.length, k = len2 - 1; j < len2; k = j++) { - p1 = part[j]; - p2 = part[k]; +/* + * CircleMarker canvas specific drawing parts. + */ - if (((p1.y > p.y) !== (p2.y > p.y)) && (p.x < (p2.x - p1.x) * (p.y - p1.y) / (p2.y - p1.y) + p1.x)) { - inside = !inside; - } - } +L.CircleMarker.include(!L.Path.CANVAS ? {} : { + _updateStyle: function () { + L.Path.prototype._updateStyle.call(this); } - - // also check if it's on polygon stroke - return inside || L.Polyline.prototype._containsPoint.call(this, p, true); -}; - -L.CircleMarker.prototype._containsPoint = function (p) { - return p.distanceTo(this._point) <= this._radius + this._clickTolerance(); -}; +}); /* @@ -6025,7 +6122,7 @@ L.GeoJSON = L.FeatureGroup.extend({ // Only add this if geometry or geometries are set and not null feature = features[i]; if (feature.geometries || feature.geometry || feature.features || feature.coordinates) { - this.addData(feature); + this.addData(features[i]); } } return this; @@ -6035,7 +6132,7 @@ L.GeoJSON = L.FeatureGroup.extend({ if (options.filter && !options.filter(geojson)) { return; } - var layer = L.GeoJSON.geometryToLayer(geojson, options); + var layer = L.GeoJSON.geometryToLayer(geojson, options.pointToLayer, options.coordsToLatLng, options); layer.feature = L.GeoJSON.asFeature(geojson); layer.defaultOptions = layer.options; @@ -6049,14 +6146,17 @@ L.GeoJSON = L.FeatureGroup.extend({ }, resetStyle: function (layer) { - // reset any custom styles - layer.options = layer.defaultOptions; - this._setLayerStyle(layer, this.options.style); - return this; + var style = this.options.style; + if (style) { + // reset any custom styles + L.Util.extend(layer.options, layer.defaultOptions); + + this._setLayerStyle(layer, style); + } }, setStyle: function (style) { - return this.eachLayer(function (layer) { + this.eachLayer(function (layer) { this._setLayerStyle(layer, style); }, this); }, @@ -6072,15 +6172,14 @@ L.GeoJSON = L.FeatureGroup.extend({ }); L.extend(L.GeoJSON, { - geometryToLayer: function (geojson, options) { - + geometryToLayer: function (geojson, pointToLayer, coordsToLatLng, vectorOptions) { var geometry = geojson.type === 'Feature' ? geojson.geometry : geojson, coords = geometry.coordinates, layers = [], - pointToLayer = options && options.pointToLayer, - coordsToLatLng = options && options.coordsToLatLng || this.coordsToLatLng, latlng, latlngs, i, len; + coordsToLatLng = coordsToLatLng || this.coordsToLatLng; + switch (geometry.type) { case 'Point': latlng = coordsToLatLng(coords); @@ -6094,14 +6193,23 @@ L.extend(L.GeoJSON, { return new L.FeatureGroup(layers); case 'LineString': - case 'MultiLineString': - latlngs = this.coordsToLatLngs(coords, geometry.type === 'LineString' ? 0 : 1, coordsToLatLng); - return new L.Polyline(latlngs, options); + latlngs = this.coordsToLatLngs(coords, 0, coordsToLatLng); + return new L.Polyline(latlngs, vectorOptions); case 'Polygon': + if (coords.length === 2 && !coords[1].length) { + throw new Error('Invalid GeoJSON object.'); + } + latlngs = this.coordsToLatLngs(coords, 1, coordsToLatLng); + return new L.Polygon(latlngs, vectorOptions); + + case 'MultiLineString': + latlngs = this.coordsToLatLngs(coords, 1, coordsToLatLng); + return new L.MultiPolyline(latlngs, vectorOptions); + case 'MultiPolygon': - latlngs = this.coordsToLatLngs(coords, geometry.type === 'Polygon' ? 1 : 2, coordsToLatLng); - return new L.Polygon(latlngs, options); + latlngs = this.coordsToLatLngs(coords, 2, coordsToLatLng); + return new L.MultiPolygon(latlngs, vectorOptions); case 'GeometryCollection': for (i = 0, len = geometry.geometries.length; i < len; i++) { @@ -6110,7 +6218,7 @@ L.extend(L.GeoJSON, { geometry: geometry.geometries[i], type: 'Feature', properties: geojson.properties - }, options)); + }, pointToLayer, coordsToLatLng, vectorOptions)); } return new L.FeatureGroup(layers); @@ -6119,14 +6227,15 @@ L.extend(L.GeoJSON, { } }, - coordsToLatLng: function (coords) { + coordsToLatLng: function (coords) { // (Array[, Boolean]) -> LatLng return new L.LatLng(coords[1], coords[0], coords[2]); }, - coordsToLatLngs: function (coords, levelsDeep, coordsToLatLng) { - var latlngs = []; + coordsToLatLngs: function (coords, levelsDeep, coordsToLatLng) { // (Array[, Number, Function]) -> Array + var latlng, i, len, + latlngs = []; - for (var i = 0, len = coords.length, latlng; i < len; i++) { + for (i = 0, len = coords.length; i < len; i++) { latlng = levelsDeep ? this.coordsToLatLngs(coords[i], levelsDeep - 1, coordsToLatLng) : (coordsToLatLng || this.coordsToLatLng)(coords[i]); @@ -6138,31 +6247,26 @@ L.extend(L.GeoJSON, { }, latLngToCoords: function (latlng) { - return latlng.alt !== undefined ? - [latlng.lng, latlng.lat, latlng.alt] : - [latlng.lng, latlng.lat]; + var coords = [latlng.lng, latlng.lat]; + + if (latlng.alt !== undefined) { + coords.push(latlng.alt); + } + return coords; }, - latLngsToCoords: function (latlngs, levelsDeep, closed) { + latLngsToCoords: function (latLngs) { var coords = []; - for (var i = 0, len = latlngs.length; i < len; i++) { - coords.push(levelsDeep ? - L.GeoJSON.latLngsToCoords(latlngs[i], levelsDeep - 1, closed): - L.GeoJSON.latLngToCoords(latlngs[i])); - } - - if (!levelsDeep && closed) { - coords.push(coords[0]); + for (var i = 0, len = latLngs.length; i < len; i++) { + coords.push(L.GeoJSON.latLngToCoords(latLngs[i])); } return coords; }, getFeature: function (layer, newGeometry) { - return layer.feature ? - L.extend({}, layer.feature, {geometry: newGeometry}) : - L.GeoJSON.asFeature(newGeometry); + return layer.feature ? L.extend({}, layer.feature, {geometry: newGeometry}) : L.GeoJSON.asFeature(newGeometry); }, asFeature: function (geoJSON) { @@ -6191,83 +6295,90 @@ L.Marker.include(PointToGeoJSON); L.Circle.include(PointToGeoJSON); L.CircleMarker.include(PointToGeoJSON); -L.Polyline.prototype.toGeoJSON = function () { - var multi = !this._flat(this._latlngs); - - var coords = L.GeoJSON.latLngsToCoords(this._latlngs, multi ? 1 : 0); +L.Polyline.include({ + toGeoJSON: function () { + return L.GeoJSON.getFeature(this, { + type: 'LineString', + coordinates: L.GeoJSON.latLngsToCoords(this.getLatLngs()) + }); + } +}); - return L.GeoJSON.getFeature(this, { - type: (multi ? 'Multi' : '') + 'LineString', - coordinates: coords - }); -}; +L.Polygon.include({ + toGeoJSON: function () { + var coords = [L.GeoJSON.latLngsToCoords(this.getLatLngs())], + i, len, hole; -L.Polygon.prototype.toGeoJSON = function () { - var holes = !this._flat(this._latlngs), - multi = holes && !this._flat(this._latlngs[0]); + coords[0].push(coords[0][0]); - var coords = L.GeoJSON.latLngsToCoords(this._latlngs, multi ? 2 : holes ? 1 : 0, true); + if (this._holes) { + for (i = 0, len = this._holes.length; i < len; i++) { + hole = L.GeoJSON.latLngsToCoords(this._holes[i]); + hole.push(hole[0]); + coords.push(hole); + } + } - if (holes && this._latlngs.length === 1) { - multi = true; - coords = [coords]; - } - if (!holes) { - coords = [coords]; + return L.GeoJSON.getFeature(this, { + type: 'Polygon', + coordinates: coords + }); } +}); - return L.GeoJSON.getFeature(this, { - type: (multi ? 'Multi' : '') + 'Polygon', - coordinates: coords - }); -}; +(function () { + function multiToGeoJSON(type) { + return function () { + var coords = []; + this.eachLayer(function (layer) { + coords.push(layer.toGeoJSON().geometry.coordinates); + }); -L.LayerGroup.include({ - toMultiPoint: function () { - var coords = []; + return L.GeoJSON.getFeature(this, { + type: type, + coordinates: coords + }); + }; + } - this.eachLayer(function (layer) { - coords.push(layer.toGeoJSON().geometry.coordinates); - }); + L.MultiPolyline.include({toGeoJSON: multiToGeoJSON('MultiLineString')}); + L.MultiPolygon.include({toGeoJSON: multiToGeoJSON('MultiPolygon')}); - return L.GeoJSON.getFeature(this, { - type: 'MultiPoint', - coordinates: coords - }); - }, + L.LayerGroup.include({ + toGeoJSON: function () { - toGeoJSON: function () { + var geometry = this.feature && this.feature.geometry, + jsons = [], + json; - var type = this.feature && this.feature.geometry && this.feature.geometry.type; + if (geometry && geometry.type === 'MultiPoint') { + return multiToGeoJSON('MultiPoint').call(this); + } - if (type === 'MultiPoint') { - return this.toMultiPoint(); - } + var isGeometryCollection = geometry && geometry.type === 'GeometryCollection'; - var isGeometryCollection = type === 'GeometryCollection', - jsons = []; + this.eachLayer(function (layer) { + if (layer.toGeoJSON) { + json = layer.toGeoJSON(); + jsons.push(isGeometryCollection ? json.geometry : L.GeoJSON.asFeature(json)); + } + }); - this.eachLayer(function (layer) { - if (layer.toGeoJSON) { - var json = layer.toGeoJSON(); - jsons.push(isGeometryCollection ? json.geometry : L.GeoJSON.asFeature(json)); + if (isGeometryCollection) { + return L.GeoJSON.getFeature(this, { + geometries: jsons, + type: 'GeometryCollection' + }); } - }); - if (isGeometryCollection) { - return L.GeoJSON.getFeature(this, { - geometries: jsons, - type: 'GeometryCollection' - }); + return { + type: 'FeatureCollection', + features: jsons + }; } - - return { - type: 'FeatureCollection', - features: jsons - }; - } -}); + }); +}()); L.geoJson = function (geojson, options) { return new L.GeoJSON(geojson, options); @@ -6276,58 +6387,22 @@ L.geoJson = function (geojson, options) { /* * L.DomEvent contains functions for working with DOM events. - * Inspired by John Resig, Dean Edwards and YUI addEvent implementations. */ -var eventsKey = '_leaflet_events'; - L.DomEvent = { + /* inspired by John Resig, Dean Edwards and YUI addEvent implementations */ + addListener: function (obj, type, fn, context) { // (HTMLElement, String, Function[, Object]) - on: function (obj, types, fn, context) { - - if (typeof types === 'object') { - for (var type in types) { - this._on(obj, type, types[type], fn); - } - } else { - types = L.Util.splitWords(types); - - for (var i = 0, len = types.length; i < len; i++) { - this._on(obj, types[i], fn, context); - } - } - - return this; - }, - - off: function (obj, types, fn, context) { - - if (typeof types === 'object') { - for (var type in types) { - this._off(obj, type, types[type], fn); - } - } else { - types = L.Util.splitWords(types); - - for (var i = 0, len = types.length; i < len; i++) { - this._off(obj, types[i], fn, context); - } - } - - return this; - }, - - _on: function (obj, type, fn, context) { - var id = type + L.stamp(fn) + (context ? '_' + L.stamp(context) : ''); + var id = L.stamp(fn), + key = '_leaflet_' + type + id, + handler, originalHandler, newType; - if (obj[eventsKey] && obj[eventsKey][id]) { return this; } + if (obj[key]) { return this; } - var handler = function (e) { - return fn.call(context || obj, e || window.event); + handler = function (e) { + return fn.call(context || obj, e || L.DomEvent._getEvent()); }; - var originalHandler = handler; - if (L.Browser.pointer && type.indexOf('touch') === 0) { return this.addPointerListener(obj, type, handler, id); } @@ -6342,19 +6417,25 @@ L.DomEvent = { obj.addEventListener(type, handler, false); } else if ((type === 'mouseenter') || (type === 'mouseleave')) { + + originalHandler = handler; + newType = (type === 'mouseenter' ? 'mouseover' : 'mouseout'); + handler = function (e) { - e = e || window.event; if (!L.DomEvent._checkMouse(obj, e)) { return; } return originalHandler(e); }; - obj.addEventListener(type === 'mouseenter' ? 'mouseover' : 'mouseout', handler, false); + obj.addEventListener(newType, handler, false); + + } else if (type === 'click' && L.Browser.android) { + originalHandler = handler; + handler = function (e) { + return L.DomEvent._filterClick(e, originalHandler); + }; + + obj.addEventListener(type, handler, false); } else { - if (type === 'click' && L.Browser.android) { - handler = function (e) { - return L.DomEvent._filterClick(e, originalHandler); - }; - } obj.addEventListener(type, handler, false); } @@ -6362,22 +6443,21 @@ L.DomEvent = { obj.attachEvent('on' + type, handler); } - obj[eventsKey] = obj[eventsKey] || {}; - obj[eventsKey][id] = handler; + obj[key] = handler; return this; }, - _off: function (obj, type, fn, context) { + removeListener: function (obj, type, fn) { // (HTMLElement, String, Function) - var id = type + L.stamp(fn) + (context ? '_' + L.stamp(context) : ''), - handler = obj[eventsKey] && obj[eventsKey][id]; + var id = L.stamp(fn), + key = '_leaflet_' + type + id, + handler = obj[key]; if (!handler) { return this; } if (L.Browser.pointer && type.indexOf('touch') === 0) { this.removePointerListener(obj, type, id); - } else if (L.Browser.touch && (type === 'dblclick') && this.removeDoubleTapListener) { this.removeDoubleTapListener(obj, id); @@ -6387,17 +6467,16 @@ L.DomEvent = { obj.removeEventListener('DOMMouseScroll', handler, false); obj.removeEventListener(type, handler, false); + } else if ((type === 'mouseenter') || (type === 'mouseleave')) { + obj.removeEventListener((type === 'mouseenter' ? 'mouseover' : 'mouseout'), handler, false); } else { - obj.removeEventListener( - type === 'mouseenter' ? 'mouseover' : - type === 'mouseleave' ? 'mouseout' : type, handler, false); + obj.removeEventListener(type, handler, false); } - } else if ('detachEvent' in obj) { obj.detachEvent('on' + type, handler); } - obj[eventsKey][id] = null; + obj[key] = null; return this; }, @@ -6415,18 +6494,23 @@ L.DomEvent = { }, disableScrollPropagation: function (el) { - return L.DomEvent.on(el, 'mousewheel MozMousePixelScroll', L.DomEvent.stopPropagation); + var stop = L.DomEvent.stopPropagation; + + return L.DomEvent + .on(el, 'mousewheel', stop) + .on(el, 'MozMousePixelScroll', stop); }, disableClickPropagation: function (el) { var stop = L.DomEvent.stopPropagation; - L.DomEvent.on(el, L.Draggable.START.join(' '), stop); + for (var i = L.Draggable.START.length - 1; i >= 0; i--) { + L.DomEvent.on(el, L.Draggable.START[i], stop); + } - return L.DomEvent.on(el, { - click: L.DomEvent._fakeStop, - dblclick: stop - }); + return L.DomEvent + .on(el, 'click', L.DomEvent._fakeStop) + .on(el, 'dblclick', stop); }, preventDefault: function (e) { @@ -6501,6 +6585,22 @@ L.DomEvent = { return (related !== el); }, + _getEvent: function () { // evil magic for IE + /*jshint noarg:false */ + var e = window.event; + if (!e) { + var caller = arguments.callee.caller; + while (caller) { + e = caller['arguments'][0]; + if (e && window.Event === e.constructor) { + break; + } + caller = caller.caller; + } + } + return e; + }, + // this is a horrible workaround for a bug in Android where a single touch triggers two click events _filterClick: function (e, handler) { var timeStamp = (e.timeStamp || e.originalEvent.timeStamp), @@ -6521,15 +6621,16 @@ L.DomEvent = { } }; -L.DomEvent.addListener = L.DomEvent.on; -L.DomEvent.removeListener = L.DomEvent.off; +L.DomEvent.on = L.DomEvent.addListener; +L.DomEvent.off = L.DomEvent.removeListener; /* * L.Draggable allows you to add dragging capabilities to any element. Supports mobile devices too. */ -L.Draggable = L.Evented.extend({ +L.Draggable = L.Class.extend({ + includes: L.Mixin.Events, statics: { START: L.Browser.touch ? ['touchstart', 'mousedown'] : ['mousedown'], @@ -6555,7 +6656,9 @@ L.Draggable = L.Evented.extend({ enable: function () { if (this._enabled) { return; } - L.DomEvent.on(this._dragStartTarget, L.Draggable.START.join(' '), this._onDown, this); + for (var i = L.Draggable.START.length - 1; i >= 0; i--) { + L.DomEvent.on(this._dragStartTarget, L.Draggable.START[i], this._onDown, this); + } this._enabled = true; }, @@ -6563,7 +6666,9 @@ L.Draggable = L.Evented.extend({ disable: function () { if (!this._enabled) { return; } - L.DomEvent.off(this._dragStartTarget, L.Draggable.START.join(' '), this._onDown, this); + for (var i = L.Draggable.START.length - 1; i >= 0; i--) { + L.DomEvent.off(this._dragStartTarget, L.Draggable.START[i], this._onDown, this); + } this._enabled = false; this._moved = false; @@ -6583,8 +6688,6 @@ L.Draggable = L.Evented.extend({ if (this._moving) { return; } - this.fire('down'); - var first = e.touches ? e.touches[0] : e; this._startPoint = new L.Point(first.clientX, first.clientY); @@ -6617,7 +6720,6 @@ L.Draggable = L.Evented.extend({ this._startPos = L.DomUtil.getPosition(this._element).subtract(offset); L.DomUtil.addClass(document.body, 'leaflet-dragging'); - this._lastTarget = e.target || e.srcElement; L.DomUtil.addClass(this._lastTarget, 'leaflet-drag-target'); } @@ -6645,8 +6747,8 @@ L.Draggable = L.Evented.extend({ for (var i in L.Draggable.MOVE) { L.DomEvent - .off(document, L.Draggable.MOVE[i], this._onMove, this) - .off(document, L.Draggable.END[i], this._onUp, this); + .off(document, L.Draggable.MOVE[i], this._onMove) + .off(document, L.Draggable.END[i], this._onUp); } L.DomUtil.enableImageDrag(); @@ -6721,10 +6823,9 @@ L.Map.Drag = L.Handler.extend({ this._draggable = new L.Draggable(map._mapPane, map._container); this._draggable.on({ - down: this._onDown, - dragstart: this._onDragStart, - drag: this._onDrag, - dragend: this._onDragEnd + 'dragstart': this._onDragStart, + 'drag': this._onDrag, + 'dragend': this._onDragEnd }, this); if (map.options.worldCopyJump) { @@ -6745,15 +6846,13 @@ L.Map.Drag = L.Handler.extend({ return this._draggable && this._draggable._moved; }, - _onDown: function () { - if (this._map._panAnim) { - this._map._panAnim.stop(); - } - }, - _onDragStart: function () { var map = this._map; + if (map._panAnim) { + map._panAnim.stop(); + } + map .fire('movestart') .fire('dragstart'); @@ -6784,11 +6883,12 @@ L.Map.Drag = L.Handler.extend({ }, _onViewReset: function () { - var pxCenter = this._map.getSize().divideBy(2), + // TODO fix hardcoded Earth values + var pxCenter = this._map.getSize()._divideBy(2), pxWorldCenter = this._map.latLngToLayerPoint([0, 0]); this._initialWorldOffset = pxWorldCenter.subtract(pxCenter).x; - this._worldWidth = this._map.getPixelWorldBounds().getSize().x; + this._worldWidth = this._map.project([0, 180]).x; }, _onPreDrag: function () { @@ -6889,30 +6989,23 @@ L.Map.addInitHook('addHandler', 'doubleClickZoom', L.Map.DoubleClickZoom); */ L.Map.mergeOptions({ - scrollWheelZoom: true, - wheelDebounceTime: 40 + scrollWheelZoom: true }); L.Map.ScrollWheelZoom = L.Handler.extend({ addHooks: function () { - L.DomEvent.on(this._map._container, { - mousewheel: this._onWheelScroll, - MozMousePixelScroll: L.DomEvent.preventDefault - }, this); - + L.DomEvent.on(this._map._container, 'mousewheel', this._onWheelScroll, this); + L.DomEvent.on(this._map._container, 'MozMousePixelScroll', L.DomEvent.preventDefault); this._delta = 0; }, removeHooks: function () { - L.DomEvent.off(this._map._container, { - mousewheel: this._onWheelScroll, - MozMousePixelScroll: L.DomEvent.preventDefault - }, this); + L.DomEvent.off(this._map._container, 'mousewheel', this._onWheelScroll); + L.DomEvent.off(this._map._container, 'MozMousePixelScroll', L.DomEvent.preventDefault); }, _onWheelScroll: function (e) { var delta = L.DomEvent.getWheelDelta(e); - var debounce = this._map.options.wheelDebounceTime; this._delta += delta; this._lastMousePos = this._map.mouseEventToContainerPoint(e); @@ -6921,12 +7014,13 @@ L.Map.ScrollWheelZoom = L.Handler.extend({ this._startTime = +new Date(); } - var left = Math.max(debounce - (+new Date() - this._startTime), 0); + var left = Math.max(40 - (+new Date() - this._startTime), 0); clearTimeout(this._timer); this._timer = setTimeout(L.bind(this._performZoom, this), left); - L.DomEvent.stop(e); + L.DomEvent.preventDefault(e); + L.DomEvent.stopPropagation(e); }, _performZoom: function () { @@ -6965,9 +7059,13 @@ L.extend(L.DomEvent, { // inspired by Zepto touch code by Thomas Fuchs addDoubleTapListener: function (obj, handler, id) { - var last, touch, + var last, doubleTap = false, delay = 250, + touch, + pre = '_leaflet_', + touchstart = this._touchstart, + touchend = this._touchend, trackedTouches = []; function onTouchStart(e) { @@ -6979,11 +7077,12 @@ L.extend(L.DomEvent, { } else { count = e.touches.length; } - - if (count > 1) { return; } + if (count > 1) { + return; + } var now = Date.now(), - delta = now - (last || now); + delta = now - (last || now); touch = e.touches ? e.touches[0] : e; doubleTap = (delta > 0 && delta <= delay); @@ -6993,19 +7092,26 @@ L.extend(L.DomEvent, { function onTouchEnd(e) { if (L.Browser.pointer) { var idx = trackedTouches.indexOf(e.pointerId); - if (idx === -1) { return; } + if (idx === -1) { + return; + } trackedTouches.splice(idx, 1); } if (doubleTap) { if (L.Browser.pointer) { // work around .type being readonly with MSPointer* events - var newTouch = {}, - prop, i; + var newTouch = { }, + prop; - for (i in touch) { + // jshint forin:false + for (var i in touch) { prop = touch[i]; - newTouch[i] = prop && prop.bind ? prop.bind(touch) : prop; + if (typeof prop === 'function') { + newTouch[i] = prop.bind(touch); + } else { + newTouch[i] = prop; + } } touch = newTouch; } @@ -7014,11 +7120,6 @@ L.extend(L.DomEvent, { last = null; } } - - var pre = '_leaflet_', - touchstart = this._touchstart, - touchend = this._touchend; - obj[pre + touchstart + id] = onTouchStart; obj[pre + touchend + id] = onTouchEnd; @@ -7027,8 +7128,8 @@ L.extend(L.DomEvent, { var endElement = L.Browser.pointer ? document.documentElement : obj; obj.addEventListener(touchstart, onTouchStart, false); - endElement.addEventListener(touchend, onTouchEnd, false); + if (L.Browser.pointer) { endElement.addEventListener(L.DomEvent.POINTER_CANCEL, onTouchEnd, false); } @@ -7037,15 +7138,15 @@ L.extend(L.DomEvent, { }, removeDoubleTapListener: function (obj, id) { - var pre = '_leaflet_', - endElement = L.Browser.pointer ? document.documentElement : obj, - touchend = obj[pre + this._touchend + id]; + var pre = '_leaflet_'; obj.removeEventListener(this._touchstart, obj[pre + this._touchstart + id], false); + (L.Browser.pointer ? document.documentElement : obj).removeEventListener( + this._touchend, obj[pre + this._touchend + id], false); - endElement.removeEventListener(this._touchend, touchend, false); if (L.Browser.pointer) { - endElement.removeEventListener(L.DomEvent.POINTER_CANCEL, touchend, false); + document.documentElement.removeEventListener(L.DomEvent.POINTER_CANCEL, obj[pre + this._touchend + id], + false); } return this; @@ -7059,105 +7160,153 @@ L.extend(L.DomEvent, { L.extend(L.DomEvent, { - POINTER_DOWN: L.Browser.msPointer ? 'MSPointerDown' : 'pointerdown', - POINTER_MOVE: L.Browser.msPointer ? 'MSPointerMove' : 'pointermove', - POINTER_UP: L.Browser.msPointer ? 'MSPointerUp' : 'pointerup', + //static + POINTER_DOWN: L.Browser.msPointer ? 'MSPointerDown' : 'pointerdown', + POINTER_MOVE: L.Browser.msPointer ? 'MSPointerMove' : 'pointermove', + POINTER_UP: L.Browser.msPointer ? 'MSPointerUp' : 'pointerup', POINTER_CANCEL: L.Browser.msPointer ? 'MSPointerCancel' : 'pointercancel', - _pointers: {}, + _pointers: [], + _pointerDocumentListener: false, // Provides a touch events wrapper for (ms)pointer events. - // ref http://www.w3.org/TR/pointerevents/ https://www.w3.org/Bugs/Public/show_bug.cgi?id=22890 + // Based on changes by veproza https://github.com/CloudMade/Leaflet/pull/1019 + //ref http://www.w3.org/TR/pointerevents/ https://www.w3.org/Bugs/Public/show_bug.cgi?id=22890 addPointerListener: function (obj, type, handler, id) { - if (type === 'touchstart') { - this._addPointerStart(obj, handler, id); + switch (type) { + case 'touchstart': + return this.addPointerListenerStart(obj, type, handler, id); + case 'touchend': + return this.addPointerListenerEnd(obj, type, handler, id); + case 'touchmove': + return this.addPointerListenerMove(obj, type, handler, id); + default: + throw 'Unknown touch event type'; + } + }, - } else if (type === 'touchmove') { - this._addPointerMove(obj, handler, id); + addPointerListenerStart: function (obj, type, handler, id) { + var pre = '_leaflet_', + pointers = this._pointers; - } else if (type === 'touchend') { - this._addPointerEnd(obj, handler, id); - } + var cb = function (e) { - return this; - }, + L.DomEvent.preventDefault(e); - removePointerListener: function (obj, type, id) { - var handler = obj['_leaflet_' + type + id]; + var alreadyInArray = false; + for (var i = 0; i < pointers.length; i++) { + if (pointers[i].pointerId === e.pointerId) { + alreadyInArray = true; + break; + } + } + if (!alreadyInArray) { + pointers.push(e); + } + + e.touches = pointers.slice(); + e.changedTouches = [e]; - if (type === 'touchstart') { - obj.removeEventListener(this.POINTER_DOWN, handler, false); + handler(e); + }; - } else if (type === 'touchmove') { - obj.removeEventListener(this.POINTER_MOVE, handler, false); + obj[pre + 'touchstart' + id] = cb; + obj.addEventListener(this.POINTER_DOWN, cb, false); + + // need to also listen for end events to keep the _pointers list accurate + // this needs to be on the body and never go away + if (!this._pointerDocumentListener) { + var internalCb = function (e) { + for (var i = 0; i < pointers.length; i++) { + if (pointers[i].pointerId === e.pointerId) { + pointers.splice(i, 1); + break; + } + } + }; + //We listen on the documentElement as any drags that end by moving the touch off the screen get fired there + document.documentElement.addEventListener(this.POINTER_UP, internalCb, false); + document.documentElement.addEventListener(this.POINTER_CANCEL, internalCb, false); - } else if (type === 'touchend') { - obj.removeEventListener(this.POINTER_UP, handler, false); - obj.removeEventListener(this.POINTER_CANCEL, handler, false); + this._pointerDocumentListener = true; } return this; }, - _addPointerStart: function (obj, handler, id) { - var onDown = L.bind(function (e) { - L.DomEvent.preventDefault(e); + addPointerListenerMove: function (obj, type, handler, id) { + var pre = '_leaflet_', + touches = this._pointers; - this._pointers[e.pointerId] = e; - this._handlePointer(e, handler); - }, this); + function cb(e) { - obj['_leaflet_touchstart' + id] = onDown; - obj.addEventListener(this.POINTER_DOWN, onDown, false); + // don't fire touch moves when mouse isn't down + if ((e.pointerType === e.MSPOINTER_TYPE_MOUSE || e.pointerType === 'mouse') && e.buttons === 0) { return; } - // need to also listen for end events to keep the _pointers object accurate - if (!this._pointerDocListener) { - var removePointer = L.bind(function (e) { - delete this._pointers[e.pointerId]; - }, this); + for (var i = 0; i < touches.length; i++) { + if (touches[i].pointerId === e.pointerId) { + touches[i] = e; + break; + } + } - // we listen documentElement as any drags that end by moving the touch off the screen get fired there - document.documentElement.addEventListener(this.POINTER_UP, removePointer, false); - document.documentElement.addEventListener(this.POINTER_CANCEL, removePointer, false); + e.touches = touches.slice(); + e.changedTouches = [e]; - this._pointerDocListener = true; + handler(e); } - }, - _handlePointer: function (e, handler) { - e.touches = []; - for (var i in this._pointers) { - e.touches.push(this._pointers[i]); - } - e.changedTouches = [e]; + obj[pre + 'touchmove' + id] = cb; + obj.addEventListener(this.POINTER_MOVE, cb, false); - handler(e); + return this; }, - _addPointerMove: function (obj, handler, id) { - var onMove = L.bind(function (e) { - // don't fire touch moves when mouse isn't down - if ((e.pointerType === e.MSPOINTER_TYPE_MOUSE || e.pointerType === 'mouse') && e.buttons === 0) { return; } + addPointerListenerEnd: function (obj, type, handler, id) { + var pre = '_leaflet_', + touches = this._pointers; - this._pointers[e.pointerId] = e; - this._handlePointer(e, handler); - }, this); + var cb = function (e) { + for (var i = 0; i < touches.length; i++) { + if (touches[i].pointerId === e.pointerId) { + touches.splice(i, 1); + break; + } + } + + e.touches = touches.slice(); + e.changedTouches = [e]; + + handler(e); + }; + + obj[pre + 'touchend' + id] = cb; + obj.addEventListener(this.POINTER_UP, cb, false); + obj.addEventListener(this.POINTER_CANCEL, cb, false); - obj['_leaflet_touchmove' + id] = onMove; - obj.addEventListener(this.POINTER_MOVE, onMove, false); + return this; }, - _addPointerEnd: function (obj, handler, id) { - var onUp = L.bind(function (e) { - delete this._pointers[e.pointerId]; - this._handlePointer(e, handler); - }, this); + removePointerListener: function (obj, type, id) { + var pre = '_leaflet_', + cb = obj[pre + type + id]; + + switch (type) { + case 'touchstart': + obj.removeEventListener(this.POINTER_DOWN, cb, false); + break; + case 'touchmove': + obj.removeEventListener(this.POINTER_MOVE, cb, false); + break; + case 'touchend': + obj.removeEventListener(this.POINTER_UP, cb, false); + obj.removeEventListener(this.POINTER_CANCEL, cb, false); + break; + } - obj['_leaflet_touchend' + id] = onUp; - obj.addEventListener(this.POINTER_UP, onUp, false); - obj.addEventListener(this.POINTER_CANCEL, onUp, false); + return this; } }); @@ -7209,22 +7358,26 @@ L.Map.TouchZoom = L.Handler.extend({ }, _onTouchMove: function (e) { + var map = this._map; + if (!e.touches || e.touches.length !== 2 || !this._zooming) { return; } - var map = this._map, - p1 = map.mouseEventToLayerPoint(e.touches[0]), + var p1 = map.mouseEventToLayerPoint(e.touches[0]), p2 = map.mouseEventToLayerPoint(e.touches[1]); this._scale = p1.distanceTo(p2) / this._startDist; this._delta = p1._add(p2)._divideBy(2)._subtract(this._startCenter); + if (this._scale === 1) { return; } + if (!map.options.bounceAtZoomLimits) { - var currentZoom = map.getScaleZoom(this._scale); - if ((currentZoom <= map.getMinZoom() && this._scale < 1) || - (currentZoom >= map.getMaxZoom() && this._scale > 1)) { return; } + if ((map.getZoom() === map.getMinZoom() && this._scale < 1) || + (map.getZoom() === map.getMaxZoom() && this._scale > 1)) { return; } } if (!this._moved) { + L.DomUtil.addClass(map._mapPane, 'leaflet-touching'); + map .fire('movestart') .fire('zoomstart'); @@ -7233,22 +7386,19 @@ L.Map.TouchZoom = L.Handler.extend({ } L.Util.cancelAnimFrame(this._animRequest); - this._animRequest = L.Util.requestAnimFrame(this._updateOnMove, this, true, this._map._container); + this._animRequest = L.Util.requestAnimFrame( + this._updateOnMove, this, true, this._map._container); L.DomEvent.preventDefault(e); }, _updateOnMove: function () { - var map = this._map; - - if (map.options.touchZoom === 'center') { - this._center = map.getCenter(); - } else { - this._center = map.layerPointToLatLng(this._getTargetCenter()); - } - this._zoom = map.getScaleZoom(this._scale); + var map = this._map, + origin = this._getScaleOrigin(), + center = map.layerPointToLatLng(origin), + zoom = map.getScaleZoom(this._scale); - map._animateZoom(this._center, this._zoom); + map._animateZoom(center, zoom, this._startCenter, this._scale, this._delta, false, true); }, _onTouchEnd: function () { @@ -7257,22 +7407,31 @@ L.Map.TouchZoom = L.Handler.extend({ return; } + var map = this._map; + this._zooming = false; + L.DomUtil.removeClass(map._mapPane, 'leaflet-touching'); L.Util.cancelAnimFrame(this._animRequest); L.DomEvent .off(document, 'touchmove', this._onTouchMove) .off(document, 'touchend', this._onTouchEnd); - var map = this._map, + var origin = this._getScaleOrigin(), + center = map.layerPointToLatLng(origin), + oldZoom = map.getZoom(), - zoomDelta = this._zoom - oldZoom, - finalZoom = map._limitZoom(oldZoom + (zoomDelta > 0 ? Math.ceil(zoomDelta) : Math.floor(zoomDelta))); + floatZoomDelta = map.getScaleZoom(this._scale) - oldZoom, + roundZoomDelta = (floatZoomDelta > 0 ? + Math.ceil(floatZoomDelta) : Math.floor(floatZoomDelta)), - map._animateZoom(this._center, finalZoom, true); + zoom = map._limitZoom(oldZoom + roundZoomDelta), + scale = map.getZoomScale(zoom) / this._scale; + + map._animateZoom(center, zoom, origin, scale); }, - _getTargetCenter: function () { + _getScaleOrigin: function () { var centerOffset = this._centerOffset.subtract(this._delta).divideBy(this._scale); return this._startCenter.add(centerOffset); } @@ -7331,22 +7490,18 @@ L.Map.Tap = L.Handler.extend({ this._simulateEvent('contextmenu', first); } }, this), 1000); - - this._simulateEvent('mousedown', first); - L.DomEvent.on(document, { - touchmove: this._onMove, - touchend: this._onUp - }, this); + L.DomEvent + .on(document, 'touchmove', this._onMove, this) + .on(document, 'touchend', this._onUp, this); }, _onUp: function (e) { clearTimeout(this._holdTimeout); - L.DomEvent.off(document, { - touchmove: this._onMove, - touchend: this._onUp - }, this); + L.DomEvent + .off(document, 'touchmove', this._onMove, this) + .off(document, 'touchend', this._onUp, this); if (this._fireClick && e && e.changedTouches) { @@ -7356,8 +7511,6 @@ L.Map.Tap = L.Handler.extend({ if (el && el.tagName && el.tagName.toLowerCase() === 'a') { L.DomUtil.removeClass(el, 'leaflet-active'); } - - this._simulateEvent('mouseup', first); // simulate click if the touch didn't move too much if (this._isTapValid()) { @@ -7410,6 +7563,7 @@ L.Map.BoxZoom = L.Handler.extend({ this._map = map; this._container = map._container; this._pane = map._panes.overlayPane; + this._moved = false; }, addHooks: function () { @@ -7417,7 +7571,8 @@ L.Map.BoxZoom = L.Handler.extend({ }, removeHooks: function () { - L.DomEvent.off(this._container, 'mousedown', this._onMouseDown, this); + L.DomEvent.off(this._container, 'mousedown', this._onMouseDown); + this._moved = false; }, moved: function () { @@ -7425,75 +7580,83 @@ L.Map.BoxZoom = L.Handler.extend({ }, _onMouseDown: function (e) { - if (!e.shiftKey || ((e.which !== 1) && (e.button !== 1))) { return false; } - this._moved = false; + if (!e.shiftKey || ((e.which !== 1) && (e.button !== 1))) { return false; } + L.DomUtil.disableTextSelection(); L.DomUtil.disableImageDrag(); - this._startPoint = this._map.mouseEventToContainerPoint(e); + this._startLayerPoint = this._map.mouseEventToLayerPoint(e); - L.DomEvent.on(document, { - contextmenu: L.DomEvent.stop, - mousemove: this._onMouseMove, - mouseup: this._onMouseUp, - keydown: this._onKeyDown - }, this); + L.DomEvent + .on(document, 'mousemove', this._onMouseMove, this) + .on(document, 'mouseup', this._onMouseUp, this) + .on(document, 'keydown', this._onKeyDown, this); }, _onMouseMove: function (e) { if (!this._moved) { - this._moved = true; - - this._box = L.DomUtil.create('div', 'leaflet-zoom-box', this._container); - L.DomUtil.addClass(this._container, 'leaflet-crosshair'); + this._box = L.DomUtil.create('div', 'leaflet-zoom-box', this._pane); + L.DomUtil.setPosition(this._box, this._startLayerPoint); + //TODO refactor: move cursor to styles + this._container.style.cursor = 'crosshair'; this._map.fire('boxzoomstart'); } - this._point = this._map.mouseEventToContainerPoint(e); + var startPoint = this._startLayerPoint, + box = this._box, + + layerPoint = this._map.mouseEventToLayerPoint(e), + offset = layerPoint.subtract(startPoint), - var bounds = new L.Bounds(this._point, this._startPoint), - size = bounds.getSize(); + newPos = new L.Point( + Math.min(layerPoint.x, startPoint.x), + Math.min(layerPoint.y, startPoint.y)); - L.DomUtil.setPosition(this._box, bounds.min); + L.DomUtil.setPosition(box, newPos); - this._box.style.width = size.x + 'px'; - this._box.style.height = size.y + 'px'; + this._moved = true; + + // TODO refactor: remove hardcoded 4 pixels + box.style.width = (Math.max(0, Math.abs(offset.x) - 4)) + 'px'; + box.style.height = (Math.max(0, Math.abs(offset.y) - 4)) + 'px'; }, _finish: function () { if (this._moved) { - L.DomUtil.remove(this._box); - L.DomUtil.removeClass(this._container, 'leaflet-crosshair'); + this._pane.removeChild(this._box); + this._container.style.cursor = ''; } L.DomUtil.enableTextSelection(); L.DomUtil.enableImageDrag(); - L.DomEvent.off(document, { - contextmenu: L.DomEvent.stop, - mousemove: this._onMouseMove, - mouseup: this._onMouseUp, - keydown: this._onKeyDown - }, this); + L.DomEvent + .off(document, 'mousemove', this._onMouseMove) + .off(document, 'mouseup', this._onMouseUp) + .off(document, 'keydown', this._onKeyDown); }, _onMouseUp: function (e) { - if ((e.which !== 1) && (e.button !== 1)) { return false; } this._finish(); - if (!this._moved) { return; } + var map = this._map, + layerPoint = map.mouseEventToLayerPoint(e); + + if (this._startLayerPoint.equals(layerPoint)) { return; } var bounds = new L.LatLngBounds( - this._map.containerPointToLatLng(this._startPoint), - this._map.containerPointToLatLng(this._point)); + map.layerPointToLatLng(this._startLayerPoint), + map.layerPointToLatLng(layerPoint)); - this._map - .fitBounds(bounds) - .fire('boxzoomend', {boxZoomBounds: bounds}); + map.fitBounds(bounds); + + map.fire('boxzoomend', { + boxZoomBounds: bounds + }); }, _onKeyDown: function (e) { @@ -7542,31 +7705,29 @@ L.Map.Keyboard = L.Handler.extend({ container.tabIndex = '0'; } - L.DomEvent.on(container, { - focus: this._onFocus, - blur: this._onBlur, - mousedown: this._onMouseDown - }, this); + L.DomEvent + .on(container, 'focus', this._onFocus, this) + .on(container, 'blur', this._onBlur, this) + .on(container, 'mousedown', this._onMouseDown, this); - this._map.on({ - focus: this._addHooks, - blur: this._removeHooks - }, this); + this._map + .on('focus', this._addHooks, this) + .on('blur', this._removeHooks, this); }, removeHooks: function () { this._removeHooks(); - L.DomEvent.off(this._map._container, { - focus: this._onFocus, - blur: this._onBlur, - mousedown: this._onMouseDown - }, this); + var container = this._map._container; - this._map.off({ - focus: this._addHooks, - blur: this._removeHooks - }, this); + L.DomEvent + .off(container, 'focus', this._onFocus, this) + .off(container, 'blur', this._onBlur, this) + .off(container, 'mousedown', this._onMouseDown, this); + + this._map + .off('focus', this._addHooks, this) + .off('blur', this._removeHooks, this); }, _onMouseDown: function () { @@ -7633,8 +7794,6 @@ L.Map.Keyboard = L.Handler.extend({ }, _onKeyDown: function (e) { - if (e.altKey || e.ctrlKey || e.metaKey) { return; } - var key = e.keyCode, map = this._map; @@ -7673,27 +7832,25 @@ L.Handler.MarkerDrag = L.Handler.extend({ addHooks: function () { var icon = this._marker._icon; - if (!this._draggable) { this._draggable = new L.Draggable(icon, icon); } - this._draggable.on({ - dragstart: this._onDragStart, - drag: this._onDrag, - dragend: this._onDragEnd - }, this).enable(); - - L.DomUtil.addClass(icon, 'leaflet-marker-draggable'); + this._draggable + .on('dragstart', this._onDragStart, this) + .on('drag', this._onDrag, this) + .on('dragend', this._onDragEnd, this); + this._draggable.enable(); + L.DomUtil.addClass(this._marker._icon, 'leaflet-marker-draggable'); }, removeHooks: function () { - this._draggable.off({ - dragstart: this._onDragStart, - drag: this._onDrag, - dragend: this._onDragEnd - }, this).disable(); + this._draggable + .off('dragstart', this._onDragStart, this) + .off('drag', this._onDrag, this) + .off('dragend', this._onDragEnd, this); + this._draggable.disable(); L.DomUtil.removeClass(this._marker._icon, 'leaflet-marker-draggable'); }, @@ -7790,15 +7947,17 @@ L.Control = L.Class.extend({ return this; }, - remove: function () { - L.DomUtil.remove(this._container); + removeFrom: function (map) { + var pos = this.getPosition(), + corner = map._controlCorners[pos]; + + corner.removeChild(this._container); + this._map = null; if (this.onRemove) { - this.onRemove(this._map); + this.onRemove(map); } - this._map = null; - return this; }, @@ -7823,7 +7982,7 @@ L.Map.include({ }, removeControl: function (control) { - control.remove(); + control.removeFrom(this); return this; }, @@ -7846,7 +8005,7 @@ L.Map.include({ }, _clearControlPos: function () { - L.DomUtil.remove(this._controlContainer); + this._container.removeChild(this._controlContainer); } }); @@ -7866,13 +8025,16 @@ L.Control.Zoom = L.Control.extend({ onAdd: function (map) { var zoomName = 'leaflet-control-zoom', - container = L.DomUtil.create('div', zoomName + ' leaflet-bar'), - options = this.options; + container = L.DomUtil.create('div', zoomName + ' leaflet-bar'); + + this._map = map; - this._zoomInButton = this._createButton(options.zoomInText, options.zoomInTitle, - zoomName + '-in', container, this._zoomIn); - this._zoomOutButton = this._createButton(options.zoomOutText, options.zoomOutTitle, - zoomName + '-out', container, this._zoomOut); + this._zoomInButton = this._createButton( + this.options.zoomInText, this.options.zoomInTitle, + zoomName + '-in', container, this._zoomIn, this); + this._zoomOutButton = this._createButton( + this.options.zoomOutText, this.options.zoomOutTitle, + zoomName + '-out', container, this._zoomOut, this); this._updateDisabled(); map.on('zoomend zoomlevelschange', this._updateDisabled, this); @@ -7892,17 +8054,21 @@ L.Control.Zoom = L.Control.extend({ this._map.zoomOut(e.shiftKey ? 3 : 1); }, - _createButton: function (html, title, className, container, fn) { + _createButton: function (html, title, className, container, fn, context) { var link = L.DomUtil.create('a', className, container); link.innerHTML = html; link.href = '#'; link.title = title; + var stop = L.DomEvent.stopPropagation; + L.DomEvent - .on(link, 'mousedown dblclick', L.DomEvent.stopPropagation) - .on(link, 'click', L.DomEvent.stop) - .on(link, 'click', fn, this) - .on(link, 'click', this._refocusOnMap, this); + .on(link, 'click', stop) + .on(link, 'mousedown', stop) + .on(link, 'dblclick', stop) + .on(link, 'click', L.DomEvent.preventDefault) + .on(link, 'click', fn, context) + .on(link, 'click', this._refocusOnMap, context); return link; }, @@ -7960,18 +8126,28 @@ L.Control.Attribution = L.Control.extend({ this._container = L.DomUtil.create('div', 'leaflet-control-attribution'); L.DomEvent.disableClickPropagation(this._container); - // TODO ugly, refactor for (var i in map._layers) { if (map._layers[i].getAttribution) { this.addAttribution(map._layers[i].getAttribution()); } } + + map + .on('layeradd', this._onLayerAdd, this) + .on('layerremove', this._onLayerRemove, this); this._update(); return this._container; }, + onRemove: function (map) { + map + .off('layeradd', this._onLayerAdd) + .off('layerremove', this._onLayerRemove); + + }, + setPrefix: function (prefix) { this.options.prefix = prefix; this._update(); @@ -8023,6 +8199,18 @@ L.Control.Attribution = L.Control.extend({ } this._container.innerHTML = prefixAndAttribs.join(' | '); + }, + + _onLayerAdd: function (e) { + if (e.layer.getAttribution) { + this.addAttribution(e.layer.getAttribution()); + } + }, + + _onLayerRemove: function (e) { + if (e.layer.getAttribution) { + this.removeAttribution(e.layer.getAttribution()); + } } }); @@ -8050,16 +8238,18 @@ L.Control.Scale = L.Control.extend({ position: 'bottomleft', maxWidth: 100, metric: true, - imperial: true - // updateWhenIdle: false + imperial: true, + updateWhenIdle: false }, onAdd: function (map) { + this._map = map; + var className = 'leaflet-control-scale', container = L.DomUtil.create('div', className), options = this.options; - this._addScales(options, className + '-line', container); + this._addScales(options, className, container); map.on(options.updateWhenIdle ? 'moveend' : 'move', this._update, this); map.whenReady(this._update, this); @@ -8073,68 +8263,76 @@ L.Control.Scale = L.Control.extend({ _addScales: function (options, className, container) { if (options.metric) { - this._mScale = L.DomUtil.create('div', className, container); + this._mScale = L.DomUtil.create('div', className + '-line', container); } if (options.imperial) { - this._iScale = L.DomUtil.create('div', className, container); + this._iScale = L.DomUtil.create('div', className + '-line', container); } }, _update: function () { - var map = this._map, - y = map.getSize().y / 2; + var bounds = this._map.getBounds(), + centerLat = bounds.getCenter().lat, + halfWorldMeters = 6378137 * Math.PI * Math.cos(centerLat * Math.PI / 180), + dist = halfWorldMeters * (bounds.getNorthEast().lng - bounds.getSouthWest().lng) / 180, - var maxMeters = L.CRS.Earth.distance( - map.containerPointToLatLng([0, y]), - map.containerPointToLatLng([this.options.maxWidth, y])); + size = this._map.getSize(), + options = this.options, + maxMeters = 0; + + if (size.x > 0) { + maxMeters = dist * (options.maxWidth / size.x); + } - this._updateScales(maxMeters); + this._updateScales(options, maxMeters); }, - _updateScales: function (maxMeters) { - if (this.options.metric && maxMeters) { + _updateScales: function (options, maxMeters) { + if (options.metric && maxMeters) { this._updateMetric(maxMeters); } - if (this.options.imperial && maxMeters) { + + if (options.imperial && maxMeters) { this._updateImperial(maxMeters); } }, _updateMetric: function (maxMeters) { - var meters = this._getRoundNum(maxMeters), - label = meters < 1000 ? meters + ' m' : (meters / 1000) + ' km'; + var meters = this._getRoundNum(maxMeters); - this._updateScale(this._mScale, label, meters / maxMeters); + this._mScale.style.width = this._getScaleWidth(meters / maxMeters) + 'px'; + this._mScale.innerHTML = meters < 1000 ? meters + ' m' : (meters / 1000) + ' km'; }, _updateImperial: function (maxMeters) { var maxFeet = maxMeters * 3.2808399, + scale = this._iScale, maxMiles, miles, feet; if (maxFeet > 5280) { maxMiles = maxFeet / 5280; miles = this._getRoundNum(maxMiles); - this._updateScale(this._iScale, miles + ' mi', miles / maxMiles); + + scale.style.width = this._getScaleWidth(miles / maxMiles) + 'px'; + scale.innerHTML = miles + ' mi'; } else { feet = this._getRoundNum(maxFeet); - this._updateScale(this._iScale, feet + ' ft', feet / maxFeet); + + scale.style.width = this._getScaleWidth(feet / maxFeet) + 'px'; + scale.innerHTML = feet + ' ft'; } }, - _updateScale: function (scale, text, ratio) { - scale.style.width = Math.round(this.options.maxWidth * ratio) + 'px'; - scale.innerHTML = text; + _getScaleWidth: function (ratio) { + return Math.round(this.options.maxWidth * ratio) - 10; }, _getRoundNum: function (num) { var pow10 = Math.pow(10, (Math.floor(num) + '').length - 1), d = num / pow10; - d = d >= 10 ? 10 : - d >= 5 ? 5 : - d >= 3 ? 3 : - d >= 2 ? 2 : 1; + d = d >= 10 ? 10 : d >= 5 ? 5 : d >= 3 ? 3 : d >= 2 ? 2 : 1; return pow10 * d; } @@ -8172,35 +8370,47 @@ L.Control.Layers = L.Control.extend({ } }, - onAdd: function () { + onAdd: function (map) { this._initLayout(); this._update(); + map + .on('layeradd', this._onLayerChange, this) + .on('layerremove', this._onLayerChange, this); + return this._container; }, + onRemove: function (map) { + map + .off('layeradd', this._onLayerChange, this) + .off('layerremove', this._onLayerChange, this); + }, + addBaseLayer: function (layer, name) { this._addLayer(layer, name); - return this._update(); + this._update(); + return this; }, addOverlay: function (layer, name) { this._addLayer(layer, name, true); - return this._update(); + this._update(); + return this; }, removeLayer: function (layer) { - layer.off('add remove', this._onLayerChange, this); - - delete this._layers[L.stamp(layer)]; - return this._update(); + var id = L.stamp(layer); + delete this._layers[id]; + this._update(); + return this; }, _initLayout: function () { var className = 'leaflet-control-layers', container = this._container = L.DomUtil.create('div', className); - // makes this work on IE touch devices by stopping it from firing a mouseout event when the touch is released + //Makes this work on IE10 Touch devices by stopping it from firing a mouseout event when the touch is released container.setAttribute('aria-haspopup', true); if (!L.Browser.touch) { @@ -8215,12 +8425,10 @@ L.Control.Layers = L.Control.extend({ if (this.options.collapsed) { if (!L.Browser.android) { - L.DomEvent.on(container, { - mouseenter: this._expand, - mouseleave: this._collapse - }, this); + L.DomEvent + .on(container, 'mouseover', this._expand, this) + .on(container, 'mouseout', this._collapse, this); } - var link = this._layersLink = L.DomUtil.create('a', className + '-toggle', container); link.href = '#'; link.title = 'Layers'; @@ -8229,11 +8437,11 @@ L.Control.Layers = L.Control.extend({ L.DomEvent .on(link, 'click', L.DomEvent.stop) .on(link, 'click', this._expand, this); - } else { + } + else { L.DomEvent.on(link, 'focus', this._expand, this); } - - // work around for Firefox Android issue https://github.com/Leaflet/Leaflet/issues/2033 + //Work around for Firefox android issue https://github.com/Leaflet/Leaflet/issues/2033 L.DomEvent.on(form, 'click', function () { setTimeout(L.bind(this._onInputClick, this), 0); }, this); @@ -8252,8 +8460,6 @@ L.Control.Layers = L.Control.extend({ }, _addLayer: function (layer, name, overlay) { - layer.on('add remove', this._onLayerChange, this); - var id = L.stamp(layer); this._layers[id] = { @@ -8269,12 +8475,16 @@ L.Control.Layers = L.Control.extend({ }, _update: function () { - if (!this._container) { return; } + if (!this._container) { + return; + } - L.DomUtil.empty(this._baseLayersList); - L.DomUtil.empty(this._overlaysList); + this._baseLayersList.innerHTML = ''; + this._overlaysList.innerHTML = ''; - var baseLayersPresent, overlaysPresent, i, obj; + var baseLayersPresent = false, + overlaysPresent = false, + i, obj; for (i in this._layers) { obj = this._layers[i]; @@ -8284,31 +8494,34 @@ L.Control.Layers = L.Control.extend({ } this._separator.style.display = overlaysPresent && baseLayersPresent ? '' : 'none'; - - return this; }, _onLayerChange: function (e) { + var obj = this._layers[L.stamp(e.layer)]; + + if (!obj) { return; } + if (!this._handlingClick) { this._update(); } - var overlay = this._layers[L.stamp(e.target)].overlay; - - var type = overlay ? - (e.type === 'add' ? 'overlayadd' : 'overlayremove') : - (e.type === 'add' ? 'baselayerchange' : null); + var type = obj.overlay ? + (e.type === 'layeradd' ? 'overlayadd' : 'overlayremove') : + (e.type === 'layeradd' ? 'baselayerchange' : null); if (type) { - this._map.fire(type, e.target); + this._map.fire(type, obj); } }, // IE7 bugs out if you create a radio dynamically, so you have to do it this hacky way (see http://bit.ly/PqYLBe) _createRadioElement: function (name, checked) { - var radioHtml = ''; + var radioHtml = '= 0) { @@ -8704,46 +8910,49 @@ L.Map.include(!zoomAnimated ? {} : { // offset is the pixel coords of the zoom origin relative to the current center var scale = this.getZoomScale(zoom), - offset = this._getCenterOffset(center)._divideBy(1 - 1 / scale); + offset = this._getCenterOffset(center)._divideBy(1 - 1 / scale), + origin = this._getCenterLayerPoint()._add(offset); // don't animate if the zoom origin isn't within one screen from the current center, unless forced if (options.animate !== true && !this.getSize().contains(offset)) { return false; } - L.Util.requestAnimFrame(function () { - this - .fire('movestart') - .fire('zoomstart') - ._animateZoom(center, zoom, true); - }, this); + this + .fire('movestart') + .fire('zoomstart'); + + this._animateZoom(center, zoom, origin, scale, null, true); return true; }, - _animateZoom: function (center, zoom, startAnim) { - if (startAnim) { + _animateZoom: function (center, zoom, origin, scale, delta, backwards, forTouchZoom) { + + if (!forTouchZoom) { this._animatingZoom = true; + } - // remember what center/zoom to set after animation - this._animateToCenter = center; - this._animateToZoom = zoom; + // put transform transition on all layers with leaflet-zoom-animated class + L.DomUtil.addClass(this._mapPane, 'leaflet-zoom-anim'); - // disable any dragging during animation - if (L.Draggable) { - L.Draggable._disabled = true; - } + // remember what center/zoom to set after animation + this._animateToCenter = center; + this._animateToZoom = zoom; - L.DomUtil.addClass(this._mapPane, 'leaflet-zoom-anim'); + // disable any dragging during animation + if (L.Draggable) { + L.Draggable._disabled = true; } - var scale = this.getZoomScale(zoom), - origin = this._getCenterLayerPoint().add(this._getCenterOffset(center)._divideBy(1 - 1 / scale)); - - this.fire('zoomanim', { - center: center, - zoom: zoom, - origin: origin, - scale: scale - }); + L.Util.requestAnimFrame(function () { + this.fire('zoomanim', { + center: center, + zoom: zoom, + origin: origin, + scale: scale, + delta: delta, + backwards: backwards + }); + }, this); }, _onZoomTransitionEnd: function () { @@ -8761,18 +8970,125 @@ L.Map.include(!zoomAnimated ? {} : { }); +/* + Zoom animation logic for L.TileLayer. +*/ + +L.TileLayer.include({ + _animateZoom: function (e) { + if (!this._animating) { + this._animating = true; + this._prepareBgBuffer(); + } + + var bg = this._bgBuffer, + transform = L.DomUtil.TRANSFORM, + initialTransform = e.delta ? L.DomUtil.getTranslateString(e.delta) : bg.style[transform], + scaleStr = L.DomUtil.getScaleString(e.scale, e.origin); + + bg.style[transform] = e.backwards ? + scaleStr + ' ' + initialTransform : + initialTransform + ' ' + scaleStr; + }, + + _endZoomAnim: function () { + var front = this._tileContainer, + bg = this._bgBuffer; + + front.style.visibility = ''; + front.parentNode.appendChild(front); // Bring to fore + + // force reflow + L.Util.falseFn(bg.offsetWidth); + + this._animating = false; + }, + + _clearBgBuffer: function () { + var map = this._map; + + if (map && !map._animatingZoom && !map.touchZoom._zooming) { + this._bgBuffer.innerHTML = ''; + this._bgBuffer.style[L.DomUtil.TRANSFORM] = ''; + } + }, + + _prepareBgBuffer: function () { + + var front = this._tileContainer, + bg = this._bgBuffer; + + // if foreground layer doesn't have many tiles but bg layer does, + // keep the existing bg layer and just zoom it some more + + var bgLoaded = this._getLoadedTilesPercentage(bg), + frontLoaded = this._getLoadedTilesPercentage(front); + + if (bg && bgLoaded > 0.5 && frontLoaded < 0.5) { + + front.style.visibility = 'hidden'; + this._stopLoadingImages(front); + return; + } + + // prepare the buffer to become the front tile pane + bg.style.visibility = 'hidden'; + bg.style[L.DomUtil.TRANSFORM] = ''; + + // switch out the current layer to be the new bg layer (and vice-versa) + this._tileContainer = bg; + bg = this._bgBuffer = front; + + this._stopLoadingImages(bg); + + //prevent bg buffer from clearing right after zoom + clearTimeout(this._clearBgBufferTimer); + }, + + _getLoadedTilesPercentage: function (container) { + var tiles = container.getElementsByTagName('img'), + i, len, count = 0; + + for (i = 0, len = tiles.length; i < len; i++) { + if (tiles[i].complete) { + count++; + } + } + return count / len; + }, + + // stops loading all tiles in the background layer + _stopLoadingImages: function (container) { + var tiles = Array.prototype.slice.call(container.getElementsByTagName('img')), + i, len, tile; + + for (i = 0, len = tiles.length; i < len; i++) { + tile = tiles[i]; + + if (!tile.complete) { + tile.onload = L.Util.falseFn; + tile.onerror = L.Util.falseFn; + tile.src = L.Util.emptyImageUrl; + + tile.parentNode.removeChild(tile); + } + } + } +}); + + /* * Provides L.Map with convenient shortcuts for using browser geolocation features. */ L.Map.include({ _defaultLocateOptions: { + watch: false, + setView: false, + maxZoom: Infinity, timeout: 10000, - watch: false - // setView: false - // maxZoom: - // maximumAge: 0 - // enableHighAccuracy: false + maximumAge: 0, + enableHighAccuracy: false }, locate: function (/*Object*/ options) { @@ -8831,7 +9147,7 @@ L.Map.include({ latlng = new L.LatLng(lat, lng), latAccuracy = 180 * pos.coords.accuracy / 40075017, - lngAccuracy = latAccuracy / Math.cos((Math.PI / 180) * lat), + lngAccuracy = latAccuracy / Math.cos(L.LatLng.DEG_TO_RAD * lat), bounds = L.latLngBounds( [lat - latAccuracy, lng - lngAccuracy], @@ -8840,8 +9156,8 @@ L.Map.include({ options = this._locateOptions; if (options.setView) { - var zoom = this.getBoundsZoom(bounds); - this.setView(latlng, options.maxZoom ? Math.min(zoom, options.maxZoom) : zoom); + var zoom = Math.min(this.getBoundsZoom(bounds), options.maxZoom); + this.setView(latlng, zoom); } var data = {